源码阅读:Antd Form

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>
);
};

useForm

生成一个 formRef,存储唯一的 FormStore,返回这个 FormStore

FormStore

FormStore 是一个类,它的作用主要是以下几点:

  1. 内部有一个 store 属性,该字段存储了收集的字段与值。
  2. 作为一个发布订阅中心,当 UI 组件的值发生变化时触发订阅的事件。
  3. 暴露出 API,如平时使用较多的 getFieldValuesetFieldValuesubmit 等。

整体上看,FormStore 的功能有些类似 redux 中的 Store

Field

Field 在 mounted 时会去调用 formStoreregisterField 方法,将自身注册到 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 }));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originTriggerFunc: any = childProps[trigger];

const control = {
...childProps,
...mergedGetValueProps(value),
};

// Add trigger
control[trigger] = (...args: EventArgs) => {
// Mark as touched
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 共享传递给所有子组件。

FieldformStore 上注册字段,订阅事件,封装 onChange 方法,在 onChange 方法中发布更新事件给 formStoreField 同时包裹子 UI 组件为高阶组件,将 formStore 中的字段 value 和 封装的 onChange 传入给子 UI 组件。

本质就是 Context + 发布订阅的运用。

思考

  1. Field 的实现是基于 CC 而不是 FC,原因是 CC 可以有实例,能够很方便的调用到内部的方法。平时 FC 写的多,可能会被 FC 的思路给限制了,有些时候用 CC 可能会更合适。
  2. useForm 维护了一个唯一的单实例,这个是一种单例模式在 Hooks 中的运用。
  3. 去除掉其他的表单逻辑,Form 是一个典型的状态管理的实现。
  4. 使用 cloneElement 劫持组件 props。一般来说,高阶组件中的子组件的 props 是由父组件传入的,如果是写在子组件上的 props 会出现覆盖的情况,而 cloneElement 可以复制出一个子组件,包括其子组件的 props ,然后重新合并 props