在作活動引擎的過程當中,發現 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 。這裏不一一列舉了,只大概讀了下代碼,沒有作實驗考證具體優化了多少。
不過既然展開就多說兩句,Antd 在 Table 上的確知足了普適地後臺需求,經過分頁能夠解決大部分性能問題。可是對於不少大數據場景,Antd 地性能實際也是很通常的,沒有對大數據場景做優化。這其中有不少地優化技巧。其中最實用地莫過於虛擬滾動。因爲 Fixed 的解決方案不一樣, 也部分形成了 Table 設計地差別。
嚴格意義來講是對 Rc-table 的改進建議,但願能夠推進 ResizeObserver 的更新。
對 Element, 但願能夠早點拿走 Fixed 這種嚴重損耗性能的設計。理論上會形成3倍的性能損耗,可是實際在更加複雜的環境下,這種性能損耗會被更加放大。
在 Table 上的設計,Antd 優於 Element ,只不過被 Rc-table 坑了,Rc-table 目前對於維護上比較滯後,老實說但願 Antd 本身實現一套 Table-core 組件。不管是哪個,目前看來都有不小的優化空間。