基于antd Table封装表格性能优化
原始需求
1、支持自定义封装的基础控件在表格中展示。
2、实现一个受控的表格组件
3、实现单元格层级的校验功能,包括必填和自定义规则的校验。
4、支持自定义事件。依据配置项为click、focus、blur、change等类型事件添加自定义事件触发钩子。
5、支持自定义属性。实现行、列、单元格层级的自定义属性配置。
6、支持500行*20列表格正常使用不卡顿
分析
基于以上几点需求,无法利用现有的UI框架提供支持,同时为增加可扩展性,也只有自己封装一个表格了。
- 首先考虑的是antd 的 table,可以基于 components 实现自定义 header、row、cell,实现按列甚至单元格的控件展示逻辑。
- 通过结合 form 可以实现单元格层级的校验。
- 对于受控和自定义事件、属性,通过额外的代码逻辑也可以实现。
- 但当测试 500行*20列数据时,发现滚动和输入都存在明显卡顿问题。
问题处理
一、表格滑动卡顿问题
分析
因 antd Table 底层采用 rc-table,会为 cell 注入 onMouseEnter/onMouseOut 事件,用于鼠标滑动时使所在行高亮,原理是操作 dom 添加/删除 classname。因此在表格上下滚动过程中会不断操作 dom 触发重绘影响性能
去除自动注入的 onMouseEnter/onMouseOut 事件实现的行高亮样式后。滚动时卡顿明显改善。但是行 hover 高亮却没有了(通过 :hover 实现的背景色效果还在,如果需要行高亮可以通过覆盖该背景色的方式实现)。
二、输入值会造成表格卡顿问题
分析
通过在 row、cell 中打印日志发现,输入框输入一个字符就会导致表格所有 cell 执行一次。
那么问题来了,究竟是什么导致一个单元格的输入会触发所有单元格重绘?
- 第一个猜测是子组件未做 memo 优化,父组件重绘导致后代组件重绘。
- 第二个猜测是 Form 导致单元格强制重绘。
首先验证第一个猜测,为 row 和 cell 包裹 React.memo,并在 row 和 cell 中打印输出,发现即使使用了 React.memo 对 props 进行浅层比较也无法阻止非必要的单元格重绘。分析其可能原因是存在特殊的对象类型的 props 导致 memo 的浅层比较失效。进一步验证自定义 memo 比较方法一刀切的阻断组件重绘(注:即使可行也不能这么做,props 变化重绘组件是肯定需要的),但发现依然无法阻止单元格重绘。那么是什么原因导致的 memo 失效?是否与 form 有关?
那么就需要验证第二个猜测了。如果只有 Table 是否还会出现一个单元格修改触发所有单元格重绘?因此针对 antd Table 做测试,移除 Form 相关逻辑,仅使用 Table 构造 500 行 20 列的数据,发现确实输入值不会触发所有单元格重绘。
那么可以确认是 Form 的加入导致的所有单元格重绘。但为什么呢?
进一步分析 Form 源码发现,FormItem 中用到了 context,Form 利用 setFeildsValue 统一管理表格数据,并通过 context 将数据同步给 FormItem,而我们知道 context 和 state 一样,是不受 memo 限制触发组件重绘的。
三、如何解决因 Form 导致的单元格重绘问题
分析
首先明确引入 Form 的原因。一是为了利用 Form 管理数据。二是为了利用 Form 自带的校验功能。
其实解决该问题的方法很清楚,那就是移除 Form 相关逻辑。但随之而来需要考虑的问题是没有 Form,相关功能如何实现。
对于利用 Form 管理数据比较好处理,稍微调整下处理逻辑,使用全局的 dataSource 统一维护数据源,提供一个方法统一修改 dataSource 即可。
对于没有 Form 又要实现校验功能比较麻烦。但是我们可以借鉴 Form 的做法自己实现一个。Form 内部实现校验也基于 async-validator 插件,如何使用参考:【插件:async-validator】基本使用
- 首先根据表格组件配置定义校验规则
- 利用 async-validator 插件实现对字段的校验功能,获得 errors 数组
- 通过操作 dom 插入或控制 props 实现单元格的精确更新,避免 form 所需的整体更新
四、如何基于 async-validator 实现校验功能
1、基于表格组态配置定义校验规则
表格组态配置存于 columns 中,需识别校验相关配置项,调用 initRules 方法以行构建校验规则
校验规则 Rules 对象类型如下,主要需要 type 表示字段类型,async-validator 会依据 type 决定自定义校验逻辑,比如 number 类型对于非数值的值就会校验失败。required 表示必填,当字段值为空时检验失败。message 自定义校验提示信息。validator 自定义校验规则,覆盖自定义校验规则。
2、基于 async-validator 将自定义校验规则rules 得到 schema 实例对象
3、执行校验函数利用 schema 实例对 dataSource 进行校验,得到错误信息
对dataSource的校验会按照行数据,一行一行的进行
4、基于错误信息,通过操作 dom 在目标单元格上插入具体错误信息,并修改单元格样式令单元格控件边框标红
操作dom确实会损耗性能,特别是在存在大量校验未通过单元格的情况下。但在分析实际情况得出不会存在大量校验未通过的数据,因此不考虑极端情况。
4.1、在 validatorHandler 方法输出包含行号和具体错误信息的数据,以供在 UI Table 外使用
对于封装的表格会提供触发校验的诸多api以供其他项目人员使用
4.2、处理校验错误信息展示的 dom 结构如下
4.3、依据错误信息对具体单元格执行 dom 操作,以展示错误提示和边框标红。
4.4、当校验通过后,需要清除展示的错误信息并取消单元格边框标红,与上述处理逻辑相反
4.5、单元格输入时,触发目标单元格的校验
继续优化
在处理上述问题的过程中,也梳理出如下可优化点,涉及加快表格渲染、减少不必要的重绘、优化新旧 props 的比对。在处理了 Form 来带的影响后,表格编辑造成的卡顿、页面滚动出现的卡顿问题得到了极大地缓解。为继续提高性能,把梳理出的优化点在实现 UI Table 的过程中也考虑进去了。
一、对 row 组件利用 React.memo 处理,避免 Table 本身的重绘触发子孙组件的不必要重绘
因 row 中仅做 tr 渲染,无复杂组件绘制和计算,且由于设置的自定义memo无法处理行选中/取消等操作的准确判断,因此暂不自定义优化逻辑。
二、对 cell 组件利用 Reac.memo 和自定义 memo 方法加以限制重绘的发生
由于 cell 还是会被 antd Table 注入额外的对象类型的 props,导致单元格的重绘,因此对单元格重绘的触发条件加以限制。
在踩了一系列雷之后,达到了可行效果。其中涉及对于序号列、选择列、操作列的不限制,对于单元格校验相关配置项的不限制,对于固定列相关配置不限制,最后依靠 record 和 rowIndex 是否发生变化决定单元格是否重绘。
三、利用 shouldCellUpdate 属性自定义单元格渲染时机
发现 antd Table.Column 提供了 shouldCellUpdate 属性用于决定是否执行 render 方法渲染组件。
四、对单元格各类型组件优化
即使尽量避免了单元格的非必要重绘,但也存在需要单元格更新的时候。因此对于需要组件更新的优化如何执行更细腻度的优化,也十分有必要。
4.1、利用 react 常用优化方法优化组件性能
利用诸如类组件下的 PureComponent、shouldComponentUpdate,函数组件中的 React.memo、useMemo、useCallback 对组件内部结构进行优化。
4.2、减少组件层级
对于表格来说,通过分析 Performance 的执行结果,发现性能消耗最大的是 Recalculate Style,style 会在外层样式变动时,触发所有关联子层级样式也跟着重新计算。
因此当组件层级过多时,导致需要重新计算的样式成倍增长,正常的单元格除了td标签外,内层还可能存在更多div、span,因此在开发时要注意减少不必要的层级。
优化效果
通过减少cell中的部分层级,受影响元素数量和recalculate style时长都有所减少
版权归原作者 CHANCE_wqp 所有, 如有侵权,请联系我们删除。