高性能和高灵活度的react表格组件

对于组件导致的性能问题,的确令我十分头疼,后面我思考了两天一夜,最终才决定使用这个方案,这个方案或许可能是对于二维数组渲染的最优解了,的确目前暂时想不出更好的解决方案了,最后使用的是双重渲染和虚拟滚动,但考虑到可能对于数据量不大的场景,可能虚拟滚动并不是一个很好的选择,为此我保留了之前的渲染模式,留给大家自己决定,你可以选择打开虚拟滚动或者关闭

该react表格经过了大幅度优化,现用法如下: `

ini 复制代码
const defaultTableData = [ 
['姓名', '年龄', '职位', '入职日期'], 
['张三', '28', '前端开发工程师', '2022-05-15'], 
['李四', '32', '产品经理', '2021-08-23'],
['王五', '45', '技术总监', '2019-11-10'], 
['赵六', '35', 'UI设计师', '2023-01-05'] 
]; 


      <Table
        Item={data}           //表格数据
        virtualScroll={true}  //是否开启虚拟滚动
        rowHeight ={ 48}      //行高
        overscan ={5}          //预渲染行数
        className="w-full overflow-auto"        // 自定义样式
      />

`

原来的组件源码如下:

`

typescript 复制代码
import React, { forwardRef } from 'react';

export interface TableProps extends React.HTMLAttributes<HTMLDivElement> {
  className?: string;
  variant?: 'default' | 'bordered' | 'striped';
}


export interface TableBodyProps extends React.HTMLAttributes<HTMLDivElement> {
  Item: (number|string|React.ReactNode)[][]|HTMLDivElement[];
  className?: string;
}

export const Table = forwardRef<HTMLDivElement, TableProps>(
  ({ className, variant = 'default', children, ...props }, ref) => {
    const baseClasses = "w-full overflow-hidden";
    
    const variantClasses = {
      default: "",
      bordered: "border border-gray-200 dark:border-gray-800 rounded-lg",
      striped: ""
    };
    
    const classes = [
      baseClasses,
      variantClasses[variant],
      className
    ].filter(Boolean).join(' ');

    return (
      <div 
        ref={ref}
        className={classes}
        data-table="root"
        {...props}
      >
        {children}
      </div>
    );
  }
);

Table.displayName = 'Table';



export const TableBody = forwardRef<HTMLDivElement, TableBodyProps>(
  ({ className, Item, ...props }, ref) => {
    const classes = [
      "w-full",
      className
    ].filter(Boolean).join(' ');

    const formatItems = () => {
      if (!Array.isArray(Item)) {
        return [];
      }
      
      if (Array.isArray(Item[0])) {
        return Item;
      }
      
      return [[...(Item as unknown as (number|string)[])]];
    };

    const formattedItems = formatItems();

    return (
      <div ref={ref} className={classes} data-table="body" {...props}>
        <table className="w-full">
          <tbody>
            {formattedItems.map((row, rowIndex) => (
              <tr 
                key={rowIndex} 
                className="border-b  border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors"
              >
                {Array.isArray(row) ? row.map((cell, cellIndex) => (
                  <td 
                    key={cellIndex} 
                    className="py-3 px-4 text-gray-700 dark:text-gray-300"
                  >
                    {cell}
                  </td>
                )):''}
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
);

TableBody.displayName = 'TableBody';

`

可以看到,这里并没有使用任何的优化手段,只是单纯的进行空判断后,直接进行二维数组渲染

以下是新的表格组件的源码: `

ini 复制代码
'use client'
import React, { forwardRef, useMemo, useCallback, useState, useEffect, useRef } from 'react';

interface TableBodyProps {
    className?: string;
    Item: (number | string | React.ReactNode)[][] | (number | string | React.ReactNode)[];
    rowHeight?: number;
    overscan?: number;
    virtualScroll?: boolean;
};

// 防抖hook 
const useDebounce = (value: number, delay: number) => {
    const [debouncedValue, setDebouncedValue] = useState(value)

    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        return () => {
            clearTimeout(handler);
        };
    }, [value, delay]);

    return debouncedValue;
};

// 虚拟滚动组件
const VirtualRow = React.memo(({
    rowData, //数据
    className,
    ...props
}: {
    rowData: (number | string | React.ReactNode)[];
    className?: string;
}) => {
    return (
        <tr
            className={`border-b border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${className}`}
            {...props}
        >
            {rowData.map((cell, cellIndex) => (
                <td
                    key={cellIndex}
                    className={`py-3 px-4 text-gray-700 dark:text-gray-300`}
                >
                    {cell}
                </td>
            ))}
        </tr>
    );
});

VirtualRow.displayName = 'VirtualRow';


export const Table = forwardRef<HTMLDivElement, TableBodyProps>(
    ({
        className,                      //样式
        Item,                           // 二维数组表格数据
        rowHeight = 48,                 // 行高
        overscan = 5,                   // 预渲染行数
        virtualScroll = true,            //默认虚拟滚动
        ...props
    }, ref) => {
        const containerRef = useRef<HTMLDivElement>(null);           //容器元素Ref引用
        const [scrollTop, setScrollTop] = useState(0);               //滚动垂直位置
        const [containerHeight, setContainerHeight] = useState(0);   //容器高度
        const debouncedScrollTop = useDebounce(scrollTop, 16)        //防抖的滚动位置


        //空判断
        const flatData = useMemo(() => {
            if (!Array.isArray(Item)) return [];
            return Array.isArray(Item[0]) ? Item : [Item];
        }, [Item]);

        // 容器高度变化侦听
        useEffect(() => {
            const updateContainerHeight = () => {
                if (containerRef.current) {
                    setContainerHeight(containerRef.current.clientHeight);// clientHeight:客户端高度,也就是可见高度
                }
            };

            updateContainerHeight();


            const resizeObserver = new ResizeObserver(updateContainerHeight);
            if (containerRef.current) {
                resizeObserver.observe(containerRef.current);// 监听容器元素尺寸变化
            }

            return () => {
                resizeObserver.disconnect();// 断开侦听,清除资源
            };
        }, []);

        const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
            setScrollTop(event.currentTarget.scrollTop);// 侦听滚动条垂直位置变化
        }, []);

        // 计算可见范围
        const visibleRange = useMemo(() => {
            if (!virtualScroll || containerHeight === 0) {
                return { start: 0, end: flatData.length };// 空侦听
            }

            const startIndex = Math.max(0, Math.floor(debouncedScrollTop / rowHeight) - overscan);// 开始索引
            const visibleRowCount = Math.ceil(containerHeight / rowHeight);// 可见行
            const endIndex = Math.min(
                flatData.length,
                startIndex + visibleRowCount + overscan * 2
            );// 结束索引

            return { start: startIndex, end: endIndex };
        }, [debouncedScrollTop, containerHeight, rowHeight, overscan, virtualScroll, flatData.length]);
        const visibleRows = useMemo(() => {
            return flatData.slice(visibleRange.start, visibleRange.end);
        }, [flatData, visibleRange.start, visibleRange.end]);// 可见区域行


        const totalHeight = useMemo(() => {
            return flatData.length * rowHeight;
        }, [flatData.length, rowHeight])//容器内所有行总高度


        //className空判断后进行合并
        const classes = useMemo(() =>
            ["w-full overflow-auto", className].filter(Boolean).join(' '),  
            [className]
        )

        return (
            <div
                ref={containerRef}
                className={classes}
                onScroll={handleScroll}
                style={{ height: '100%', minHeight: '200px' }}
                {...props}
            >
                {virtualScroll ? (
                    <div
                        ref={ref}
                        style={{
                            position: 'relative',
                            height: totalHeight
                        }}
                    >
                        <table className="w-full" style={{ position: 'absolute', top: 0, left: 0, right: 0 }}>
                            <tbody>
                                {visibleRows.map((row, relativeIndex) => {
                                    const absoluteIndex = visibleRange.start + relativeIndex;

                                    return (
                                        <VirtualRow
                                            key={absoluteIndex}
                                            rowData={Array.isArray(row) ? row : [row]}
                                            className={className}
                                        />
                                    );
                                })}
                            </tbody>
                        </table>
                    </div>
                ) : (
                    <div ref={ref}>
                        <table className="w-full">
                            <tbody>
                                {flatData.map((row, rowIndex) => (
                                    <tr
                                        key={rowIndex}
                                        className={`border-b border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors ${className}`}
                                    >
                                        {Array.isArray(row) && row.map((cell, cellIndex) => (
                                            <td
                                                key={cellIndex}
                                                className={`py-3 px-4 text-gray-700 dark:text-gray-300 `}
                                            >
                                                {cell}
                                            </td>
                                        ))}
                                    </tr>
                                ))}
                            </tbody>
                        </table>
                    </div>
                )}
            </div>
        );
    }
);

`

这里主要使用了虚拟滚动来优化性能,当然,你也可以选择关闭虚拟滚动,避免过度优化

使用如下

javascript 复制代码
    const stripedTableData = [
        ['图片', '图片内容'],
        [
            <div key="vercel">
                <img src="/vercel.svg" alt="table" className='w-14 h-14 bg-black ' />
            </div>
            ,
            'vercel'
            ,
        ],
        [
            <div key="next">
                <img src="/next.svg" alt="table" />
            </div>
            ,
            'next.js'
            ,
        ],
        [
            <div key="file">
                <img src="/file.svg" alt="table" className='w-14 h-14 ' />
            </div>
            ,
            'flie'
            ,
        ],

    ];



<Table className="rounded-lg overflow-hidden">
    <TableBody
        Item={stripedTableData}
        className="bg-card"
    />
</Table>

组件十分灵活,而且上手简单,你可以自由选择写正常数据或者给某一个单元格定制化而写入jsx,你也可以做一些表格的图片和视频展示和各种个性化的表格需求,也可以数据和jsx混合使用,这都在允许的范围

表格的组件进行了优化后,使它接近了像ant的表格组件的开发体验,甚至可能比ant表格组件更加简便,但是一样不失他本身独有的灵活性,但表格可能功能不如ant完善,需要逐步添加

但新表格的源码有点多,可能需要单独写一篇了,这里不详细介绍了,其实我标了注释了,只要认真看一下也知道新组件的作用的什么的

这里也感谢你可以看到这样,下次见了,拜拜!

相关推荐
你打不到我呢3 小时前
nestjs入门:上手数据库与prisma
前端
多啦C梦a3 小时前
React 实战:从 setInterval 到 useInterval,一次搞懂定时器 Hook(还能暂停!)
前端·javascript·react.js
闲不住的李先森3 小时前
乐观更新
前端·react.js·设计模式
笔尖的记忆4 小时前
【前端架构和框架】react组件化&数据流
前端·面试
zhangzelin8884 小时前
TypeScript入门指南:JavaScript的类型化超集
前端·javascript·其他·typescript
lichenyang4534 小时前
流式聊天界面实现解析:从零到一构建实时对话体验
前端
天蓝色的鱼鱼4 小时前
Turbopack vs Webpack vs Vite:前端构建工具三分天下,谁将胜出?
前端·webpack
用户841794814564 小时前
vxe-table 实现列头授权自定义插槽模板,自定义输入框
前端
im_AMBER4 小时前
Web 开发 24
前端·笔记·git·学习