本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
最近在写 Form 组件时参考了 rc-field-form 的实现。 但是发现在大数据量的场景下 rc-field-form 的性能问题仍然很严重。以下是 rc-field-form 渲染 3000 个表单项时的效果:

从上图中可以看出,在渲染了3000个 input 时,rc-field-form 在进行频繁更新时已经出现了严重的卡顿现象。再看看在经过我们一系列优化后的效果,这里是在线地址

一、是 React 太慢了吗?
我们尝试运行以下代码:

从上图中可以看到,如果不考虑其他逻辑。React 在渲染 6000 个 input 时才会出现略微的卡顿现象。
二、rc-field-form 原理
那么为什么 rc-field-form 仅仅渲染了 3000 个 input 就会如此卡顿呢? 下面我们将简单分析一下 rc-field-form 的原理。
1、useForm

从上图可知:useForm Hook 主要是创建了一个 FormStore 对象并对外暴露了一些内部方法。

这里的方法就比较熟悉了,大部分是我们经常用到的。具体实现这里按下不表。
2、Form

由此可知 Form 主要是使用 Context 为 Children 提供了一些方法与属性。
3、Field

Field组件竟然是一个Class。Field组件主要是调用了FieldContext的一些方法,即FormStore的方法。onSoteChange方法中根据info.type的不同来决定是否应该更新。
4、FormStore

所以在 Field 组件中为其 Children 提供的 onChange 事件中使用的 dispatch 方法的本质是对所有注册的 Field 调用自身的 onStoreChange 进行遍历。同时处理字段的 dependencies 逻辑
5、总结
以上,我们可以对 rc-field-form 有一个初步的认识:
Form组件使用FieldContext.Provider为Field组件提供了方法。Field组件使用registerField直接注册了自身。Field组件对Children属性进行处理后添加了value,onChange等其他属性后返回。rc-field-form组件更新的逻辑就是一个循环。
三、原因分析
在第二节我们分析了 rc-field-form 的基本原理。认识到了 Field 是如何更新自身的。所以我们可以知道 rc-field-form 发生卡顿的原因: dispatch 方法执行时间过长:

可以看到在渲染 3000个表单项时 dispatch 所花费的时间大致在 20+ms 左右。这显然太慢了,那么该如何优化呢?
四、尝试优化
1、Field 组件什么时候会主动 render ?
在开始之前我们需要重新梳理一下 Field 组件什么时候会主动更新:
- 字段值发生变化。
shouldUpdate属性满足要求。- meta 属性发生变更。
此时我们不考虑其他情况,假设表单所有的组件都相互完全独立,那么我们的 dispatch 函数可能会变成下面的样子:

接着我们再加上 shouldUpdate 的逻辑:

可以看到我们并没有使用 onStoreChange 函数。那么要如何根据 action.type 进行不同的操作呢?

我们可以像使用 Redux 一样,直接在 dispatch 时根据类型做出相应的处理,从而减少在循环中的判断过程。
另外 Field 的 meta 属性发生变更时,字段也会重新的 render 。 我们先略过这种情况,因为这是属于字段校验的逻辑。
2、Field 组件什么时候会被动 render ?
Field 被动 render 在 rc-field-form 中只有一种可能,那就是 dependencies 属性。
本节尝试优化 dependencies 相关的逻辑,我们先观察原有的逻辑执行时间:

哪怕我们没有设置一个具有 dependencies 的表单项,这里的执行时间仍然有 6-8ms, 以下是 triggerDependenciesUpdate 大致的逻辑:

所以每次 dispatch 时都会重新收集一遍 dependencies 信息,再然后去寻找依赖字段。那么能否提前得到 dependencies2fields 呢?
其实我们可以监听 Field 的 dependencies 属性,然后更新的时候就不必每次重新去收集所有的依赖信息了,只有当 dependencies 改变时我们才会重新计算。
3. 其他优化
在实现了上述所有的优化项后。我们发现 rc-field-form 的表现仍然不是很理想。但已经比之前好多了:

不过距离我们的目标仍然差一截。这个可能是 onValuesChange 与 onFieldsChange 这两个 api 的原因。

如果表单项过多的话,我们获取 allFields 与 allValues 的时间也会成倍的增加。或许我们可以考虑传一个函数而不是帮用户计算好?或者干脆删除第二个参数,反正 useForm 都提供了这个方法。我们尝试下删除第二个参数:

包括使用 React Hook 重写 Field 组件,等其他优化项均可在此处体验。在渲染了 3000 个表单项时, dispatch 一次可以在 1-1.5ms 以内完成。