文章内容及代码基于antd4.x,文末附上源码,配合源码阅读效果更佳。阅读本文你会收获:一.Form的核心原理。二.手把手带你写一个简易版的Form
Antd Form核心原理
在antd Form中,存在一个高于所有Field组件的Store仓库用于存储表单值。通过向Field的子组件中传递Value以及onChange实现Store的与组件值的双向绑定。当Store的值发生变化,对应变动name的Field的Value跟随变动并触发Field的forceUpdata以更新视图。
一. Field注入的3个参数
众所周知FormItem会默认往我们的组件Props属性中放入3个参数:id, value, onChange。要想弄清楚form的原理,就需要先了解这3个参数。
1.id
<Form.Item
name='name'
rules={[{ required: true, message: 'Missing first name' }]}
>
<Form.Item
name={['name','firstName']}
rules={[{ required: true, message: 'Missing first name' }]}
>
<MyInput></MyInput>
</Form.Item>
</Form.Item>
复制代码
如上嵌套FormItem,最终在我们自定义组件MyInput的props中会得到id:'name_firstName'。id属性一般场景下并不会被用到。在一些特殊场景下,我们可以通过id进行解析获取到当前字段的pathName来进行后续操作。
2.value
表单Store数据变动时,form会往对应namePath的Field中传递新的Value值以更新子组件的值。
3.onChange(trigger)
触发器默认值为:onChange。监听子组件变动的触发器,当子组件值发生变动,通过触发器修改Store的值。
二. 手写简易版Form
基于以上得出原理,我们可以通过实现一个简易版本的Form来加深理解,去掉List、多层字段嵌套、校验功能等功能,实现一个Store与field Value双向响应的form。
UseForm
let store: Store = {} // form全局数据仓库
const fieldEntities: FieldEntities = [] // field实例列表
getFieldsValue() // 获取store的current value
notifyObservers(namePath) // store值变动时,通知对应field forceUpdata
setFieldsValue(newValue) // 全量赋值store
registerField(entity) // 每个field实例化时进行注册记录进fieldEntities。
dispatch({
type: 'updateValue',
namePath: 'xx',
value: 'newFieldValue'
}) // 用于更新指定name值
useForm(form) // 绑定form hook
复制代码
UseForm声明了form的全局数据仓库store以及field实例列表fieldEntities。提供了store仓库的增删改查函数,store仓库被操作后通过bian libianli
FieldContext
const context = React.createContext<FormInstance>({
getFieldsValue: warningFunc,
setFieldsValue: warningFunc,
registerField: warningFunc,
dispatch: warningFunc,
})
export default context
复制代码
应用于form全局的上下文属性。
- getFieldsValue 获取全量Store
function getFieldsValue(){
let result: Store = {}
fieldEntities.forEach((field:any)=>{
const name = field.name
result[name] = store[name]?store[name]: undefined
})
return result
}
复制代码
根据field实例列表补充返回值
- setFieldsValue 设置全量Store
function setFieldsValue(newValue: Store){
fieldEntities.forEach((entity)=>{
entity.onStoreChange()
})
return store = newValue
}
复制代码
遍历实例列表通知field forceUpdata
- updateValue 更新特定name的值
function updateValue(namePath:any, value:any){
store[namePath] = value
notifyObservers(namePath)
}
复制代码
每次值变动则通知field更新
Field
const returnChildNode = React.cloneElement(
children,
getControlled(children.props)
);
return <React.Fragment>{returnChildNode}</React.Fragment>;
复制代码
在Field需要对子组件props属性的注入Value以及onChange。
- getControlled
function getControlled(childProps: Record<string, any>) {
const formValue = getFieldsValue();
const mergedGetValueProps = {
value: formValue[name],
};
const control: Record<string, any> = {
...childProps,
...mergedGetValueProps,
};
control[trigger] = (...args: any[]) => {
let eventValue = defaultGetValueFromEvent("value", ...args);
dispatch({
type: "updateValue",
namePath: name,
value: eventValue,
});
};
return control;
}
复制代码
通过结构赋值覆盖组件的value以及onChange属性。监听Field子组件的trigger,将修改内容更新至store中。
Form
function Form({ children, form }: FormProps) {
const [formInstance] = useForm(form);
const formContextValue: FormInstance = formInstance;
const wrapperNode = (
<FieldContext.Provider value={formContextValue}>
{children}
</FieldContext.Provider>
);
return <div>{wrapperNode}</div>;
}
复制代码
绑定Form的hook,一个form hook对应一个Form实例。
小结
antd form巧妙的利用forceUpdata实现了store与Field之间的双向更新,避开了react的state。实例Form之后提供不算多的操作函数,却又足够开发者使用,namPath的数组设置则适配了更加复杂的场景。
为了让读者更容易理解,本次实现去掉了许多Antd Form的核心功能,诸如:FormList,校验器等。感兴趣的同学可自行参考其源码。