前言
Form 是项目中经常使用的组件,为了能熟练驾驭它,深入 Form 源码还是有所必要的。我们知道,在 Ant Design 4 之后,Form 的底层使用的是 rc-field-form,因此在了解源码时,我们也得对其底层的依赖深挖一番,这样才能了解的更透彻。
rc-field-form
💻 仓库地址:【传送门】(建议动手运行一下,会了解的更快)
⚠️ 注:表单的所有操作都离不开顶层的
FormStore,所有的变动信息最终都会汇聚在FormStore内。
整体结构
在了解源码之前,我们先大致了解一下 rc-field-form 的结构,因为 Ant Design Form 是基于此封装的,因此熟悉 rc-field-form 对我们解读源码来说很重要。
页面渲染的结构树如下:

基于此,我们使用 ReactDom 的方式用代码描述大致为:
1 | /** |
结构解析
根据 rc-field-form 源码的渲染逻辑,这里我花了一张结构图:

其渲染的步骤大致就是:
- 通过
useForm创建顶层的FormStore存储库,然后通过FieldContext向下传递; Field获取到由FieldContext传下的顶层FormStore,获取需要的name和一些操作方法,对DomeElement做克隆和属性封装;- 根据最终传入的
Dom类型,做相应的格式化处理和呈现;(有可能传入的为函数)
能力拆解
整个 rc-field-form 的能力可简单拆解为:
数据存储:在Form顶层维护了一个Context存储库,对Form内注册的任何子表单项的变动,都通过注册好的get、set方法来存入store内;表单对象:顶层通过useRef生成一个不随组件更新的formInstance对象,然后通过useForm生成,当初次渲染时创建出FormStore绑定即可,后续如果需要刷新则触发初始化绑定的forceUpdate方法即可刷新(我们通过form拿到的表单对象就是这个formInstance);表单校验:使用async-validate插件库校验,拼接格式则是通过name+rules字段来生成(组件注册阶段时生成组件描述数组即能方便的获取结构);变更事件:表单项的变更事件是在注册Field时绑定的,将传入的children进行cloneElement操作,生成目标子元素并改写props(通过getControlled方法去改写props以及增加校验等操作)。命名获取:表单项有个name字段是用于定义表单名的,层级越深的表单要设置其name时,需要通过getNamePath方法来拼接成最终的name,最终 join 生成的namePath大致为prefixPath_currentName
其中,formInstance 暴露出来的能力为(顶层 FormStore 暴露的能力):

而这也可以算是 rc-field-form 对外暴露的所有能力了(内部拼接 name 的方法是 getNamePath,建议留心一下)。
深入 AntD Form
AntD Form 组件及能力

图中有 AntD 在 4.20 支持的新特性:useWatch、useFormInstance:
useFormInstance:返回useForm获取的form对象,这对于子组件拿顶层form对象来说很方便;useWatch:监听字段变化返回更新后的值,仅当改字段变化时重新渲染(内部用useEffect做初始化赋值)。
⏰ 暴露的
Form.Provider一般自己封装,相当于就是使用rc-field-form自己手写Provider层一样,一般用不着。
Form 结构
AntD Form 整体基于 rc-field-form 封装了一层,其拓展了对于 AntD 其余组件使用的能力和样式的填装。我们看个简单例子:

这里大致可分为四层:
Form Provider:此部分实现了rc-field-form需要手写的部分;Custom Provider:此部分主要为AntD需要的样式控制的Context;Field Provider:沿用rc-field-form的FieldContext,添加了一些“错误呈现”、“dependencies 依赖”等内容;Content:主要做值绑定和一些校验结果、样式等的呈现,内部有自定义的关联Context;
InternalForm 相关内容
A. 内部调用的 useForm 封装了什么?

useForm 额外增添的内容不多,一个是 __INTERNAL__,它的作用就是为 React.forwardRef 暴露通过 useForm 生成的对象(可以了解一下 useImperativeHandle)。
B. 它在 Form 的基础上又做了什么?
- 初始化了很多关于样式控制相关的内容(这里将 disable、layout、size 等配置均归属于样式配置);
- 向外暴露 ref 对象;
- 添加
scroll错误定位; - 自动绑定
rc-field-form需要用户写的Provider;
FormItem 相关内容
A. FormItem 主要作用
- 决定表单的呈现方式:
函数式、组件式、数组; - 组件更新判断,局部更新处理;
- 校验相关的绑定;
总而言之,就是将
Field组件做AntD的本地化处理(通过函数子组件获取Field解析好的所有配置)。
B. FormList 的对 FormItem 的封装有什么?
FormList 其实就是调用 rc-field-form 的列表操作,然后让前端自己去对应封装 FormItem。
rc-field-form 的 List 组件则是对外暴露了一个函数(我们用官网 List 组件常用的那个函数),它最终操作的就是 Field 组件暴露出来的能力。
可动手的点
从 AntD 对外暴露的能力来看,我们基本没法基于它包装内容,因为它基本上就是包装 rc-field-form,却又没有对外面暴露 rc-field-form 支持的能力。因此通过锁定版本,根据其依赖的 rc-field-form 版本来扩充一些能力,而这就是 @alife/form-tools 封装出的原因。目前业务上使用表单比较难受的点有:
form对象的深层传入,深层次组件的namePath不好监听(4.20.0 版本已支持)。- 对于兄弟组件间的事件消息处理的很难受,特别是值变更事件(4.20.0 版本已支持);
- 对象类表单数据的
数组命名法很难受,无法复用表单组件; - 复杂表单的初始化逻辑均收敛在上层,一些需要根据值做接口查询呈现的组件开发起来很困难;
那么基于这些难点,我们根据 rc-field-form 内部暴露的一些能力可做一些封装。
form 对象跨层级传递
4.20 版本解决了跨层级传递 form 对象的问题,直接使用 Form.useFormInstance 获取到 form 对象,不用为之前需要通过 props 传值而烦恼了。
🌰 使用示例:示例传送门
但是它也有缺陷:
- 获取
form对象就得用到全量的NamePath,难做到表单组件复用。
☕️ 如果需要将表单组件抽离出来做公共组件(它们仅包裹层的
name不同),那么内部使用form的一些方法就会很难受(因为没法知道它的prefixName到底是什么)。
因此可基于 rc-field-form 做一层优化:直接获取当前路径下的 form 对象,即“自动前缀补全”。
1 | import { useCallback, useContext, useMemo } from 'react'; |
这样即使组件拆分出来,也可以不用关注 Form 命名前缀的问题,直接使用即可(数组在套用了 FormAccess 之后也适用此方法)。
兄弟组件事件处理
我们知道,如果要获取到兄弟组件间值的变更,设置此类事件监听方法很是头疼,不过在 4.20 版本提供的 useWatch 方法能够比较方便的解决这个问题,同时其获取值的能力是通过 useEffect 获取的,初始化时也能监听到变更。
🌰 使用示例:示例传送门
useWatch 的缺陷(同 4.1 所述问题)
- 一旦需要复用组件,限定死的
NamePath会导致组件最终耦合太大,无法拆分。
因此同样可按照 4.1 中的处理方式,注册个虚拟的 Field ,然后通过它来做事件监听,从而模拟 dependencies 的能力:
1 | useEffect(() => { |
更为舒适的命名方式
先前提到过,FormItem 内的表单 name 值的获取其实是用 Field 的函数方式来获取到 prefixName,最终拼接好 props 上传入的 name 来设置全名的,整体的使用很被动,这就导致:
获取值、监听事件等都需要拼接全名,对于复杂表单来说很麻烦;
数组形式的命名,会使得表单难以解耦,难以做到表单模块的复用;
rc-field-form 拼接名称的方式:
WrapperField获取到当前fieldContext的prefixName值,再传入Field组件根据传入的name来拼接;
既然它的 prefixName 是来自 fieldContext 的,而 fieldContext 的 value 又来自 formContextValue,且 fieldContext 内部并没包裹额外逻辑,那么我们为何在包裹表单对象的时候再包一层 FieldContext,直接敲定它的 prefixName,这样不就能优化掉 Form.Item 的数组命名方式吗?
@alife/form-tools 的功能拓展参考
1 | /** |
使用方法示例
1 | // 值为: { obj: { test: xxx } } |