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。