react Antd Table 多选大数据量 UI渲染很慢的解决方案

问题: react 在选择大量的数据后,继续点击页面的选中会出现UI渲染很慢的情况

完全将 UI 隔离的方案

代码:

js 复制代码
import { Table, Button, Space, Tag } from 'antd';
import React, { useState, useEffect, useRef, useCallback } from 'react';

/**
 * 模拟 API 请求模块
 * @param {object} params - 请求参数,包含 page 和 pageSize
 * @returns {Promise<{records: object[], total: number}>}
 */
const mockApi = {
  fetchList: ({ page, pageSize }) => {
    console.log(`Fetching data for page: ${page}, pageSize: ${pageSize}`);
    return new Promise((resolve) => {
      const totalRecords = 50000; // 模拟一个非常大的数据集
      const start = (page - 1) * pageSize;

      // 生成当前页的模拟数据
      const records = Array.from({ length: pageSize }, (_, i) => {
        const index = start + i;
        if (index >= totalRecords) return null; // 避免超出总数
        return {
          baseId: `id_${index + 1}`, // 保证 key 的唯一性
          name: `项目 ${index + 1}`,
          category: ['A', 'B', 'C'][index % 3],
          status: ['Active', 'Inactive', 'Archived'][index % 3],
        };
      }).filter(Boolean); // 过滤掉 null 值

      // 模拟网络延迟
      setTimeout(() => {
        resolve({ records, total: totalRecords });
      }, 300);
    });
  },
};

/**
 * 高性能跨页选择表格组件
 */
function HighPerformanceSelectableTable() {
  const [loading, setLoading] = useState(false);
  const [dataSource, setDataSource] = useState([]); // State: 仅存储当前页的表格数据
  const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });

  // 核心 State 1: 存储所有页面已选中的 key,使用 Set 数据结构以获得最佳性能
  const [allSelectedRowKeys, setAllSelectedRowKeys] = useState(new Set());

  // 核心 State 2: 仅存储当前页已选中的 key,直接用于驱动 Table UI,保证 UI 响应速度
  const [currentPageSelectedRowKeys, setCurrentPageSelectedRowKeys] = useState([]);

  // 核心 Ref: 数据缓存。使用 useRef 创建一个在组件生命周期内持久的 Map 对象
  // 用于存储所有已加载的数据,且其变化不会触发组件重新渲染
  const dataCache = useRef(new Map());

  // 表格列定义
  const columns = [
    { title: '项目 ID', dataIndex: 'baseId', width: 200 },
    { title: '项目名称', dataIndex: 'name', width: 250 },
    { title: '分类', dataIndex: 'category', width: 150 },
    {
      title: '状态',
      dataIndex: 'status',
      render: (status) => <Tag color={status === 'Active' ? 'green' : 'red'}>{status}</Tag>,
    },
  ];

  // 数据获取函数,使用 useCallback 避免不必要的函数重建
  const fetchData = useCallback(async (params) => {
    setLoading(true);
    const result = await mockApi.fetchList({
      page: params.current,
      pageSize: params.pageSize,
    });
    setLoading(false);

    if (result && result.records) {
      setDataSource(result.records);
      setPagination((prev) => ({ ...prev, ...params, total: result.total }));

      // 将新获取的数据存入缓存,以便后续根据 key 能找到完整的 row 数据
      result.records.forEach((item) => {
        dataCache.current.set(item.baseId, item);
      });
    }
  }, []);

  // 首次加载数据
  useEffect(() => {
    fetchData({ current: 1, pageSize: 10 });
  }, [fetchData]);

  // 处理分页、排序、筛选变化
  const handleTableChange = (newPagination) => {
    fetchData({ current: newPagination.current, pageSize: newPagination.pageSize });
  };

  // 【关键同步逻辑】
  // 当 `dataSource` (翻页) 或 `allSelectedRowKeys` (外部操作) 变化时,
  // 需要重新计算当前页应该展示的勾选状态。
  useEffect(() => {
    const keysToShowOnCurrentPage = dataSource
      .map((item) => item.baseId)
      .filter((key) => allSelectedRowKeys.has(key));
    setCurrentPageSelectedRowKeys(keysToShowOnCurrentPage);
  }, [dataSource, allSelectedRowKeys]);

  // 【核心选择逻辑】
  // 当用户在表格中进行勾选操作时触发
  const handleRowSelectionChange = (selectedKeysOnCurrentPage) => {
    // 步骤 1: 立刻更新当前页的 UI State,确保勾选框的即时响应
    setCurrentPageSelectedRowKeys(selectedKeysOnCurrentPage);

    // 步骤 2: 异步更新全局的已选 key 集合 (Set)
    setAllSelectedRowKeys((prevAllKeys) => {
      const newAllKeys = new Set(prevAllKeys); // 复制一份,保证 state 的不可变性
      const currentPageKeys = dataSource.map((item) => item.baseId);

      // 遍历当前页的所有 key
      currentPageKeys.forEach((key) => {
        // 如果这个 key 在当前页的最新选中项里,就添加到全局 Set 中
        if (selectedKeysOnCurrentPage.includes(key)) {
          newAllKeys.add(key);
        } else {
          // 否则(即未选中或被取消选中),就从全局 Set 中移除
          newAllKeys.delete(key);
        }
      });
      return newAllKeys;
    });
  };

  // Table 的 rowSelection prop 配置
  const rowSelection = {
    selectedRowKeys: currentPageSelectedRowKeys, // **关键**: 只将当前页的选中 keys 传给 Table
    onChange: handleRowSelectionChange,
    // (可选) 增加 getCheckboxProps 来自定义禁用逻辑
    // getCheckboxProps: (record) => ({
    //   disabled: record.status === 'Archived',
    // }),
  };

  // 点击按钮获取所有选中项
  const showAllSelected = () => {
    // 从 Set 转换为数组
    const allKeysArray = Array.from(allSelectedRowKeys);
    // 从缓存中根据 key 查找完整的行数据
    const allSelectedRows = allKeysArray.map((key) => dataCache.current.get(key)).filter(Boolean);

    console.group('所有选中项详情');
    console.log('总数:', allSelectedRows.length);
    console.log('所有选中的 Keys:', allKeysArray);
    console.log('所有选中的 Rows:', allSelectedRows);
    console.groupEnd();
    // alert(`已选择 ${allSelectedRows.length} 条记录,详情请查看控制台。`);
  };

  // 清空所有选择
  const clearAllSelection = () => {
    setAllSelectedRowKeys(new Set());
    // currentPageSelectedRowKeys 会通过 useEffect 自动同步变为空数组
  };

  return (
    <div style={{ padding: '20px' }}>
      <Space direction="vertical" style={{ width: '100%' }}>
        <Space>
          <Button type="primary" onClick={showAllSelected} disabled={allSelectedRowKeys.size === 0}>
            获取所有选中项
          </Button>
          <Button onClick={clearAllSelection} disabled={allSelectedRowKeys.size === 0}>
            清空所有选择
          </Button>
        </Space>

        <div style={{ fontWeight: 'bold' }}>
          总共已选择 <Tag color="blue">{allSelectedRowKeys.size}</Tag> 项
        </div>

        <Table
          size="middle"
          rowKey="baseId"
          loading={loading}
          columns={columns}
          dataSource={dataSource}
          rowSelection={rowSelection}
          pagination={pagination}
          onChange={handleTableChange}
          bordered
          scroll={{
            y: 'calc(100vh - 615px)',
          }}
        />
      </Space>
    </div>
  );
}

export default HighPerformanceSelectableTable;