react-window构造的虚拟列表使用react-resizable动态调整宽度和使用react-drag-listview拖拽变换列位置的问题

文章目录

    • react-window构造的虚拟列表使用react-resizable动态调整宽度和使用react-drag-listview拖拽变换列位置的问题
      • 需求
      • 问题
      • 问题根源
      • 部分代码
      • 参考

react-window构造的虚拟列表使用react-resizable动态调整宽度和使用react-drag-listview拖拽变换列位置的问题

需求

项目中使用react-windowVariableSizeGrid构造虚拟列表来解决大型的数据列表渲染的性能问题。虚拟列表的优点是不用全部加载出所有的DOM节点, 而是按需显示思路的一种实现,即虚拟列表是一种根据滚动容器元素的可视区域来渲染长列表数据中某一个部分数据的技术。具体关于虚拟列表的使用和原理这里就不过多赘述了。

ant design官网中就是使用react-window去构造虚拟列表的

react-window构造的虚拟列表使用react-resizable动态调整宽度和使用react-drag-listview拖拽变换列位置的问题_第1张图片

然后我们的需求是,想要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 实现虚拟列表

浅说虚拟列表的实现原理

你可能感兴趣的:(React.js,react.js,javascript,前端,虚拟列表)