在作活动引擎的过程当中,发现 Antd 的 Table 组件会发送各类行错位,和列错位。关于行错位不是本节内容介绍的重点,本文主要介绍在启用固定列的时候(即便用fixed) 时发生的列错位 bug 以及其衍生的一系列性能问题。javascript
下图是使用固定列和 Image 做为列内容时产生的现象,该案例很是容易复现。 css
代码java
import { Table } from 'antd';
const columns = [
{
title: 'Full Name',
width: 100,
dataIndex: 'name',
key: 'name',
fixed: 'left',
},
{
title: 'Age',
width: 100,
dataIndex: 'age',
key: 'age',
fixed: 'left',
},
{ title: 'Column 1', dataIndex: 'address', key: '1' },
{ title: 'Column 2', dataIndex: 'address', key: '2' },
{ title: 'Column 3', dataIndex: 'address', key: '3' },
{ title: 'Column 4', dataIndex: 'address', key: '4' },
{ title: 'Column 5', dataIndex: 'address', key: '5' },
{
title: 'Avatar',
width: 200,
dataIndex: 'img',
key: 'img',
render: (a, row ,b) => (
<img src={row.img}></img>
)
},
{ title: 'Column 6', dataIndex: 'address', key: '6' },
{ title: 'Column 7', dataIndex: 'address', key: '7' },
{ title: 'Column 8', dataIndex: 'address', key: '8' },
{
title: 'Action',
key: 'operation',
fixed: 'right',
width: 100,
render: () => <a href="javascript:;">action</a>,
},
];
const data = [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York Park',
img: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566139960708&di=b71fcbe1841e966f5fd3983197628ff9&imgtype=0&src=http%3A%2F%2Fpic1.16xx8.com%2Fallimg%2F161122%2F1F0035M6-7.jpg'
},
{
key: '2',
name: 'Jim Green',
age: 40,
address: 'London Park',
img: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566139968891&di=c0b7ecc441817226dabc251d870f6d22&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F01a949581aeb9fa84a0d304fd05eeb.jpg'
},
];
ReactDOM.render(<Table columns={columns} dataSource={data} scroll={{ x: 1300 }} />, mountNode);
复制代码
然而一样的状况在之前使用 Element 时几乎不会复现,因而我使用了 Element-React ,果真 Element-React 不管怎么玩都不会出现列错位或者行错位。浏览器
一般来讲,主流的固定列渲染方法无一例外都是将固定列的 column 渲染为单独的 Table 组件。而后使用绝对定位(position:absolute)将其固定在 Table 的左右两侧。bash
绝对布局会形成 Fixed Table 和 Main Table 之间列元素的 Layout 信息(如 cell height,cell width)割裂。必须使用某种方式同步割裂的信息。 这里 Element - React 和 Antd 对于 Table 在 fixed 的实现差距是很大的。antd
同一个 Table 会被渲染成三份,里面的 Dom 节点,样式彻底同样,不一样的是 Fixed 的 Table 部分是不 Visiable 的。app
能够说 Element - React 对于的 Table 为了弥补样式错位的问题,巨大地牺牲了性能的问题。这样的性能问题会在列数和列元素复杂度提高时,表现出来。函数
先看 Element - React 关于 Fixed 属性的渲染后结构: 布局
下面用直观图来表示这种设计:性能
能够看到 主Table 和 左右Fixed Table 的结构是如出一辙的,能够说很浪费了。。。 原本只须要渲染对应的 Table 列就能够了,但不得不说 Element 的作法的确解决了这个同步 Layout 信息的问题。
Antd 的 Table 内核使用了 Rc-table 组件。Rc-table 不会渲染彻底相同的 3 份Table,而是只渲染须要的 column,能够看到这种设计才是合理的。。。
而后虽然设计合理,可是 Antd 却产生很是多错位 bug。能够归结于 Rc-table 与 其他独立 Antd 组件之间,发生了一些配合的失误。这里依然只瞄准列错位的 bug。
直接去 Rc-table 的源码找到了同步固定列 Table 高度的代码段:
syncFixedTableRowHeight = () => {
//...
//搜寻主 Table 全部行元素
const bodyRows = this.bodyTable.querySelectorAll(`.${prefixCls}-row`) || [];
//...
const state = this.store.getState();
//获取主 Table 的行高
const fixedColumnsBodyRowsHeight = [].reduce.call(
bodyRows,
(acc, row) => {
const rowKey = row.getAttribute('data-row-key');
const height =
row.getBoundingClientRect().height || state.fixedColumnsBodyRowsHeight[rowKey] || 'auto';
acc[rowKey] = height;
return acc;
},
{},
);
//比较是否发生了,若是没有发生变化,就返回
if (
shallowequal(state.fixedColumnsHeadRowsHeight, fixedColumnsHeadRowsHeight) &&
shallowequal(state.fixedColumnsBodyRowsHeight, fixedColumnsBodyRowsHeight)
) {
return;
}
//若是发生了变化,就同步变化
this.store.setState({
fixedColumnsHeadRowsHeight,
fixedColumnsBodyRowsHeight,
});
};
复制代码
它会在 mounted 的时候调用,和 document 的 resize 事件调用。
但这里有一个 bug,mounted 的时候还有不少元素没有渲染出来时,如 Image。syncFixedTableRowHeight 同步时就不会计算图片地高度,这样就会产生高度割裂,虽然触发了函数,但没有同步高度的问题。
因而了解原理以后,这里天然而然能够想到给 Image 显式地指定 css height。这样一来,在 mounted 地时候就能够 调用 syncFixedTableRowHeight 获取高度,即使图片尚未渲染出来。
完美解决方法是:使用 ResizeObserver,Antd 的几乎全部错位问题,几乎都被这个方法解决了,(也许时由于浏览器兼容性,目前这个 PR 躺了大半年了,但我以为是维护不及时...),即使是兼容性问题,ResizeObserver有基于MutationObserver的polyfill,而主流浏览器对MutationObserv是支持的。
下面是解决方案地代码,之后若是造轮子,能够参考:
createObserver() {
return new ResizeObserver(entries => {
const state = this.store.getState();
const fixedColumnsHeadRowsHeight = { ...state.fixedColumnsHeadRowsHeight };
const fixedColumnsBodyRowsHeight = { ...state.fixedColumnsBodyRowsHeight };
const firstRowCellsWidth = { ...state.firstRowCellsWidth };
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const { target } = entry;
const headerRowIndex = target.getAttribute('data-header-row-index')
const rowKey = target.getAttribute('data-row-key');
const columnKey = target.getAttribute('data-column-Key')
const { width, height } = target.getBoundingClientRect();
if (headerRowIndex !== null) {
if (fixedColumnsHeadRowsHeight[headerRowIndex] !== height) {
fixedColumnsHeadRowsHeight[headerRowIndex] = height;
}
}
if (rowKey !== null) {
if (fixedColumnsBodyRowsHeight[rowKey] !== height) {
fixedColumnsBodyRowsHeight[rowKey] = height;
}
}
if (columnKey !== null) {
if (
firstRowCellsWidth[columnKey] === undefined ||
width !== firstRowCellsWidth[columnKey]
) {
firstRowCellsWidth[columnKey] = width;
}
}
}
this.store.setState({
fixedColumnsHeadRowsHeight,
fixedColumnsBodyRowsHeight,
firstRowCellsWidth,
});
});
}
复制代码
这种动态属性相似于 scrollTop,scrollLeft,hoverCellIndex 等等。 这里 Element 和 Antd 的实现是如出一辙的。
好比如何同步三个 Table 的 onScroll 属性,咱们能够监听主 Table 的 onScroll 事件。而后将主 Table 的 scrollTop 和 scrollLeft 分发到左右 Fixed table 上。这样的结果就是引起3次重绘。
syncScroll() {
const { headerWrapper, footerWrapper, bodyWrapper, fixedBodyWrapper, rightFixedBodyWrapper } = this;
if (headerWrapper) {
headerWrapper.scrollLeft = bodyWrapper.scrollLeft;
}
if (footerWrapper) {
footerWrapper.scrollLeft = bodyWrapper.scrollLeft;
}
if (fixedBodyWrapper) {
fixedBodyWrapper.scrollTop = bodyWrapper.scrollTop;
}
if (rightFixedBodyWrapper) {
rightFixedBodyWrapper.scrollTop = bodyWrapper.scrollTop;
}
}
// 主 Table
<div
style={this.bodyWrapperHeight}
className="el-table__body-wrapper"
ref={this.bindRef('bodyWrapper')}
onScroll={this.syncScroll}
>
<TableBody
{...this.props}
style={{ width: this.bodyWidth }}
/>
</div>
复制代码
这里直接进入性能比较环节,来证实 Element 在固定列上地设计对性能有多么大地损耗。
下面是从初始化到不断滚动,触发 onScroll 重绘的 Performance 截图。(0 - 10ms)
先看调用调用栈和总体流程时间占比:
这个截图能够证实 Element Table 在 Fixed 属性上的设计是有很大问题,大部分的时间都花费在渲染上,这与其设计有很是大的关系。 假如咱们把 Fixed 属性拿走,再来测试一下:
能够发现渲染上的性能差距很是大:
下面能够看到 Antd 在 Table 上的表现好的不是一点点。渲染时间很是短,仔细看代码地话,Antd 和 Rc-table 在一些细节上下了功夫,如 debounce 和 throttle 。这里不一一列举了,只大概读了下代码,没有作实验考证具体优化了多少。
因为 Fixed 的解决方案不一样, 也部分形成了 Table 设计地差别。
严格意义来讲是对 Rc-table 的改进建议,但愿能够推进 ResizeObserver 的更新。
对 Element, 但愿能够早点拿走 Fixed 这种严重损耗性能的设计。理论上会形成3倍的性能损耗,可是实际在更加复杂的环境下,这种性能损耗会被更加放大。
在 Table 上的设计,Antd 优于 Element ,只不过被 Rc-table 坑了,Rc-table 目前对于维护上比较滞后,老实说但愿 Antd 本身实现一套 Table-core 组件。不管是哪个,目前看来都有不小的优化空间。