高性能和高灵活度的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完善,需要逐步添加

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

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

相关推荐
裴嘉靖1 分钟前
Vue 生成 PDF 完整教程
前端·vue.js·pdf
毕设小屋vx ylw2824264 分钟前
Java开发、Java Web应用、前端技术及Vue项目
java·前端·vue.js
冴羽1 小时前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁1 小时前
Angular【router路由】
前端·javascript·angular.js
brzhang1 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
西洼工作室2 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js
广州华水科技2 小时前
北斗形变监测传感器在水库安全中的应用及技术优势分析
前端
开发者如是说2 小时前
Compose 开发桌面程序的一些问题
前端·架构
旺代2 小时前
Token 存储与安全防护
前端
洋不写bug3 小时前
html实现简历信息填写界面
前端·html