B端开发中,Antd Form 组件是一个常用的表单组件,它的功能很强大,使用起来也很简单,这里就来看看它的源码。
使用案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| export default () => { const [form] = Form.useForm();
return ( <Form form={form} preserve={false} onFieldsChange={fields => { console.error('fields:', fields); }} > <Field<FormData> name="name"> <Input placeholder="Username" /> </Field>
<Field<FormData> dependencies={['name']}> {() => { return form.getFieldValue('name') === '1' ? ( <Field name="password"> <Input placeholder="Password" /> </Field> ) : null; }} </Field>
<Field dependencies={['password']}> {() => { const password = form.getFieldValue('password'); console.log('>>>', password); return password ? ( <Field<FormData> name={['password2']}> <Input placeholder="Password 2" /> </Field> ) : null; }} </Field>
<button type="submit">Submit</button> </Form> ); };
|
生成一个 formRef
,存储唯一的 FormStore
,返回这个 FormStore
FormStore
是一个类,它的作用主要是以下几点:
- 内部有一个
store
属性,该字段存储了收集的字段与值。
- 作为一个发布订阅中心,当 UI 组件的值发生变化时触发订阅的事件。
- 暴露出 API,如平时使用较多的
getFieldValue
、setFieldValue
、submit
等。
整体上看,FormStore
的功能有些类似 redux
中的 Store
。
Field
Field
在 mounted 时会去调用 formStore
的 registerField
方法,将自身注册到 formStore
中, 同时返回一个取消注册的方法,这里可以看出发布订阅的模式。
Field
主要功能是从 store
中获取字段值,同时劫持 onChange
等 trigger 事件,传给 UI 组件,onChange
中会去更新 store
的值。
以上逻辑在 Field
组件下的 getControlled
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public getControlled = (childProps: ChildProps = {}) => { const { trigger, validateTrigger, getValueFromEvent, normalize, valuePropName, getValueProps, fieldContext, } = this.props;
const namePath = this.getNamePath(); const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext; const { dispatch } = getInternalHooks(HOOK_MARK); const value = this.getValue(); const mergedGetValueProps = getValueProps || ((val: StoreValue) => ({ [valuePropName]: val }));
const originTriggerFunc: any = childProps[trigger];
const control = { ...childProps, ...mergedGetValueProps(value), };
control[trigger] = (...args: EventArgs) => { this.touched = true; this.dirty = true;
this.triggerMetaEvent();
let newValue: StoreValue; if (getValueFromEvent) { newValue = getValueFromEvent(...args); } else { newValue = defaultGetValueFromEvent(valuePropName, ...args); }
if (normalize) { newValue = normalize(newValue, value, getFieldsValue(true)); }
dispatch({ type: 'updateValue', namePath, value: newValue, });
if (originTriggerFunc) { originTriggerFunc(...args); } };
return control; };
|
此外, Field
实现了 FieldEntity
这个接口, 该接口中的 onStoreChange
方法负责当 store
中对应字段的值更新后,去 reRender 对应的 Field
组件和其子组件,这样就实现了 UI 的更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| export interface FieldEntity { onStoreChange: ( store: Store, namePathList: InternalNamePath[] | null, info: ValuedNotifyInfo, ) => void; isFieldTouched: () => boolean; isFieldDirty: () => boolean; isFieldValidating: () => boolean; isListField: () => boolean; isList: () => boolean; isPreserve: () => boolean; validateRules: (options?: InternalValidateOptions) => Promise<RuleError[]>; getMeta: () => Meta; getNamePath: () => InternalNamePath; getErrors: () => string[]; getWarnings: () => string[]; props: { name?: NamePath; rules?: Rule[]; dependencies?: NamePath[]; initialValue?: any; }; }
|
一句话总结
useForm
创建一个 状态存储 formStore
, 内部维护表单的字段、字段值、发布订阅、需要暴露的 api 等。
Form
组件使用 Context
包裹子组件,将 formStore
共享传递给所有子组件。
Field
在 formStore
上注册字段,订阅事件,封装 onChange
方法,在 onChange
方法中发布更新事件给 formStore
。Field
同时包裹子 UI 组件为高阶组件,将 formStore
中的字段 value
和 封装的 onChange
传入给子 UI 组件。
本质就是 Context
+ 发布订阅的运用。
思考
Field
的实现是基于 CC
而不是 FC
,原因是 CC
可以有实例,能够很方便的调用到内部的方法。平时 FC
写的多,可能会被 FC
的思路给限制了,有些时候用 CC
可能会更合适。
useForm
维护了一个唯一的单实例,这个是一种单例模式在 Hooks 中的运用。
- 去除掉其他的表单逻辑,Form 是一个典型的状态管理的实现。
- 使用
cloneElement
劫持组件 props
。一般来说,高阶组件中的子组件的 props
是由父组件传入的,如果是写在子组件上的 props
会出现覆盖的情况,而 cloneElement
可以复制出一个子组件,包括其子组件的 props
,然后重新合并 props
。