本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
最近在写 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
以内完成。