市面上的React
表单组件一大堆,都很好使,最近打算开发一个小的ui库,表单组件自己写肯定很复杂,于是选择了用Ant Disign
的表单实现,虽然没自己写,但是搞清原理还是重要的
那么其实是一个标题党了,本文探索的是field-form,众所周知,Ant Design
的大部分组件都是基于react-component
实现的,Form
组件也是基于rc-field-form
实现的,然后再增加了一些功能,如果能够搞懂rc-field-form
的逻辑,那么就能知道Ant Design
里的Form
组件是如何运作的
<Form> <Field name="username"> <Input placeholder="Username" /> </Field> <Field name="password"> <Input placeholder="Password" /> </Field> </Form> 复制代码
field-form
的实现还是基于Context
,如果Context
配合Hooks
使用,在方便的同时有一个比较大的问题,就是它会全部刷新,有一个值改变了,即使组件没有用这个值,也会刷新,field-form
是怎么做的呢?
form-field
自己实现了表单值的存储、组件的更新,只用了Context
来传递方法
// useForm.ts // 创建了处理表单的FormStore实例 const formStore: FormStore = new FormStore(forceReRender); // 调用getForm()方法获取方法,以免一些内部属性被访问 formRef.current = formStore.getForm(); 复制代码
// Form.tsx // 通过useForm获取formRef.current的值,用context将方法传递下去,每一个Field字段组件都能获取到这些方法 <FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider> 复制代码
FormStore
是处理表单的实例,包括整个表单的值Store
,以及更新Store
的方法,那么FormStore
是一个独立于React
之外的类,Field
组件需要将刷新组件的方法注册进FormStore
// Field.tsx public componentDidMount() { const { shouldUpdate } = this.props; // 通过context获取FormStore实例的内部方法 const { getInternalHooks }: InternalFormInstance = this.context; const { registerField } = getInternalHooks(HOOK_MARK); // 注册当前Field组件实例到Form this.cancelRegisterFunc = registerField(this); } 复制代码
Field
组件是Class
而不是Hooks
,原因是Hooks
得写太多额外的代码了,Class
可以轻松的将整个Field
组件的注册进FormStore
,这样在FormStore
里就可以调用Field
组件的方法了
在值的改变处理这一块和Redux
很相似,Field
组件的值改变时,派发一个dispatch
,FormStore
收到后改变值,通知所有注册的Field
组件去对比值,如果需要作出更新就调用React
中的this.forceUpdate()
来强制刷新组件
首先,得把value
和onChange
传递给表单元素组件
// Field.tsx // 创建onChange函数和`value`值的字段 public getControlled = (childProps: ChildProps = {}) => { // 为了方便阅读,删改了部分代码 // 当前的namePath const { getInternalHooks }: InternalFormInstance = this.context; const { dispatch } = getInternalHooks(HOOK_MARK); const value = this.getValue(); // children表单元素组件上本来的trigger函数 // 如果children本来就有onChange,被Field受控后还是会调用的 const originTriggerFunc: any = childProps.onChange; // 这是要传给children的props const control = { ...childProps, value, }; // 更改值的函数,默认为onChange control.onChange = (...args: EventArgs) => { // 默认取值是event.target.value let newValue: StoreValue = defaultGetValueFromEvent(valuePropName, ...args); // ... // 派发dispatch,同时FormStore更新store的值 dispatch({ type: 'updateValue', namePath, value: newValue, }); // 调用children本来的函数 if (originTriggerFunc) { originTriggerFunc(...args); } }; // ... return control; }; 复制代码
通常组件都是onChange
和value
这两个受控字段,在getControlled()
方法中,创建了这两个字段,在onChange
中通知Store
改变,并且还会调用组件上原有的onChange
,同时将组件原有的props
在传递下去
在渲染的时候将这两个字段传递给受控的表单元素组件
// Field.tsx // 为了方便阅读,删改了部分代码 // 渲染Field组件和受控的表单元素组件 public render() { const { children } = this.props; let returnChildNode: React.ReactNode = React.cloneElement( children as React.ReactElement, //child.props为参数,同时将onChange和value传递给组件 this.getControlled((children as React.ReactElement).props), ); return <React.Fragment>{returnChildNode}</React.Fragment>; } 复制代码
在FormStore
的表单值改变时,还需要通知所有注册上的Field
组件更新
// useForm.tsx // class FormStore // Field组件通过调用dispatch来更新值 private dispatch = (action: ReducerAction) => { switch (action.type) { case 'updateValue': { const { namePath, value } = action; // 更新值 this.updateValue(namePath, value); break; } // ... }; // 更新值 private updateValue = (name: NamePath, value: StoreValue) => { const namePath = getNamePath(name); const prevStore = this.store; // 对值进行一个深拷贝 this.store = setValue(this.store, namePath, value); // 通知注册的Field组件更新 this.notifyObservers(prevStore, [namePath], { type: 'valueUpdate', source: 'internal', }); // ... }; // 通知观察者,也就是通知所有的Field组件 private notifyObservers = ( prevStore: Store, namePathList: InternalNamePath[] | null, info: NotifyInfo, ) => { // 当前store的值和NotifyInfo const mergedInfo: ValuedNotifyInfo = { ...info, store: this.getFieldsValue(true), }; // 获取所有的字段实例,调用其中的onStoreChange // 字段实例就是Field组件的React实例,注册的时候直接将Field的this注册进来了 this.getFieldEntities().forEach(({ onStoreChange }) => { onStoreChange(prevStore, namePathList, mergedInfo); }); }; 复制代码
先是Field
组件值改变,派发dispatch
来改变FormStore
的值,dispatch
内部调用notifyObservers
方法将参数传递通知每一个Field
组件
所以最后值改变了应该怎么做还是回到了Field
组件
// Field.tsx public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => { const { shouldUpdate } = this.props; const { store } = info; const namePath = this.getNamePath(); // 当前字段上一次的值 const prevValue = this.getValue(prevStore); // 当前字段的值 const curValue = this.getValue(store); // 当前字段NamePath是否在namePathList中 const namePathMatch = namePathList && containsNamePath(namePathList, namePath); switch (info.type) { // ... default: // 如果值有改变就刷新组件 if ( namePathMatch || dependencies.some(dependency => containsNamePath(namePathList, getNamePath(dependency)), ) || requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info) ) { this.reRender(); return; } break; } // ... }; 复制代码
只包括值是如何改变的基本逻辑就是这样,还是蛮绕的
对我来说最大的缺点是没能提供useField
,在hooks
泛滥的年代,缺少了这种方式,使用的还是render props
,多少有点不习惯,可能因为一开始就没这个需求,所以注册Field
直接注册了一个class
实例,除非暴露store
改变的订阅事件,这样应该会好做一点
浅析不了了,头大,断断续续看了一个月,结果今天想总结下,发现都不记得了,只记得一点简单的逻辑,脑壳疼
果然奔着为看源码而看源码并没有太大用,还是得需要做相同的功能再来看源码参考实现比较靠谱