项目中使用react-window
的VariableSizeGrid
构造虚拟列表来解决大型的数据列表渲染的性能问题。虚拟列表的优点是不用全部加载出所有的DOM节点, 而是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。具体关于虚拟列表的使用和原理这里就不过多赘述了。
ant design官网中就是使用react-window
去构造虚拟列表的
然后我们的需求是,想要react-resizable
去动态调整table(虚拟列表)的列宽度,还有使用react-drag-listview
拖拽变换列位置。关于具体实现,这里不过多介绍,网上有很多参考的例子:Ant Design + react-drag-listview实现Table拖拽变换列位置
然而在实际开发中发现,对于普通Table(非虚拟列表)是生效的,能够动态的改变列的宽度和位置,然而对虚拟列表却无法变化。
在VariableSizeGrid
会遇到虚拟列表项样式缓存没有被清除导致和第一次可视区域里展示的一样。
我们可以看到VariableSizeGrid
中的两个API
/**
* VariableSizeGrid caches offsets and measurements for each column index for performance purposes.
* This method clears that cached data for all columns after (and including) the specified index.
* It should be called whenever a column's width changes. (Note that this is not a typical occurrence.)
*
* By default the grid will automatically re-render after the index is reset.
* If you would like to delay this re-render until e.g. a state update has completed in the parent component,
* specify a value of false for the second, optional parameter.
*/
resetAfterColumnIndex(index: number, shouldForceUpdate?: boolean): void;
/**
* VariableSizeGrid caches offsets and measurements for each row index for performance purposes.
* This method clears that cached data for all rows after (and including) the specified index.
* It should be called whenever a row's height changes. (Note that this is not a typical occurrence.)
*
* By default the grid will automatically re-render after the index is reset.
* If you would like to delay this re-render until e.g. a state update has completed in the parent component,
* specify a value of false for the second, optional parameter.
*/
resetAfterRowIndex(index: number, shouldForceUpdate?: boolean): void;
大概意思就是出于性能优化的目的,VariableSizeGrid
会缓存列表的行高和列框, 所以当我们调整了列的宽度,但是却没有清楚掉这些缓存,就会导致虚拟列表不会渲染出来最新的样式。
所以我们可以手动调用这两个API来达到动态调整宽度和变化列位置。
其中核心代码是下面这句
const refreshVirtualTable = ()=>{
if (gridRef?.current) {
(gridRef.current as any)?.resetAfterRowIndex(0);
(gridRef.current as any)?.resetAfterColumnIndex(0);
}
}
下面给出部分代码
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import 'antd/dist/antd.css';
import './index.css';
import {VariableSizeGrid as Grid} from 'react-window';
import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer';
import DragTable from "@/components/common/DragTable";
function VirtualTable(props: any) {
const normalRowHeight = 25;
const {columns, scroll, rowHeight} = props;
const [tableWidth, setTableWidth] = useState(0);
const gridRef = useRef(null);
const virtualTableHiddenColumn = 'virtualTableHiddenColumn';
const [mergedColumns, setMergedColumns] = useState<any[]>([]);
const [finalColumns, setFinalColumns] = useState<any[]>([]);
const refreshVirtualTable = ()=>{
if (gridRef?.current) {
(gridRef.current as any)?.resetAfterRowIndex(0);
(gridRef.current as any)?.resetAfterColumnIndex(0);
}
}
const getTotalColumnWidth = ()=>{
let width = 0;
columns.forEach((value: any) => {
width = width + (value?.width || 0);
})
return width;
}
useLayoutEffect(() => {
const totalWidth = getTotalColumnWidth();
const allColumns = [...columns, {
title: '',
dataIndex: virtualTableHiddenColumn,
disableFilterColumnOptions: true,
disableDrag: true,
width: (totalWidth - tableWidth) > 0 ? totalWidth - tableWidth : 0,
render: () => {
return (<></>);
}
}]
setMergedColumns(allColumns.map((column: any) => {
return column;
}));
setFinalColumns(allColumns);
refreshVirtualTable();
}, [columns])
useEffect(() => {
refreshVirtualTable();
}, [rowHeight])
const renderCell = (columnIndex: number, rowIndex: number, rawData: any[][]) => {
return mergedColumns[columnIndex].render(
rawData[rowIndex][mergedColumns[columnIndex].dataIndex],
rawData[rowIndex], rowIndex
)
}
const calculateTableHeight = (rowLengths: number): number => {
if (rowLengths * (rowHeight || normalRowHeight) > scroll.y) {
return scroll.y;
} else {
let columnTotalWidth = 0;
mergedColumns.forEach((element: any) => {
columnTotalWidth += element.width;
})
return rowLengths * (rowHeight || normalRowHeight) + (columnTotalWidth > tableWidth ? 18 : 0)
}
}
const getTotalWidthExcludeHiddenColumns = () => {
let width = 0;
mergedColumns.forEach((value) => {
if (value?.dataIndex !== virtualTableHiddenColumn) {
width = width + (value?.width || 0);
}
})
return width;
}
const getColumnWidth = (totalHeight: number, index: number, width: number) => {
if (totalHeight > scroll.y && index === mergedColumns.length - 1) {
const lastColumnWidth = tableWidth - getTotalWidthExcludeHiddenColumns() - 15;
return lastColumnWidth < 0 ? 0 : lastColumnWidth;
} else if (index === mergedColumns.length - 1) {
const lastColumnWidth = tableWidth - getTotalWidthExcludeHiddenColumns();
return lastColumnWidth < 0 ? 0 : lastColumnWidth;
} else {
return width
}
}
const renderVirtualList = (rawData: any[][], {onScroll}: any) => {
const totalHeight = rawData.length * (rowHeight || normalRowHeight);
return (
<Grid ref={gridRef}
className="virtual-grid"
columnCount={mergedColumns.length}
columnWidth={(index) => {
const {width} = mergedColumns[index];
return getColumnWidth(totalHeight, index, width);
}}
height={calculateTableHeight(rawData.length)}
rowCount={rawData.length}
rowHeight={() => rowHeight || normalRowHeight}
width={tableWidth}
onScroll={({scrollLeft}) => {
onScroll({
scrollLeft,
});
}}
>
{({columnIndex, rowIndex, style}) => (
<div
className={classNames({
'zebra-odd': rowIndex % 2 !== 0
}, 'virtual-table-cell', {
'virtual-table-cell-last':
columnIndex === mergedColumns.length - 1,
}
)}
style={style}
>
{
renderCell(columnIndex, rowIndex, rawData)
}
</div>
)}
</Grid>
);
};
const onColumnChange = (column: any[]) => {
setMergedColumns(column);
refreshVirtualTable();
}
return (
<ResizeObserver
onResize={({width}) => {
setTableWidth(width);
}}
>
<DragTable
{...props}
onColumnChange={onColumnChange}
className={"virtual-table common-table"}
columns={finalColumns}
pagination={false}
components={{
body: renderVirtualList,
}}
/>
</ResizeObserver>
);
}
export default VirtualTable;
Ant Design + react-drag-listview实现Table拖拽变换列位置
Ant Design + react-resizable实现列表页可拖拽宽度变化
使用react-window构造虚拟列表(性能优化)
mini react-window(二) 实现可知变化高度虚拟列表
长列表优化:用 React 实现虚拟列表
浅说虚拟列表的实现原理