对于组件导致的性能问题,的确令我十分头疼,后面我思考了两天一夜,最终才决定使用这个方案,这个方案或许可能是对于二维数组渲染的最优解了,的确目前暂时想不出更好的解决方案了,最后使用的是双重渲染和虚拟滚动,但考虑到可能对于数据量不大的场景,可能虚拟滚动并不是一个很好的选择,为此我保留了之前的渲染模式,留给大家自己决定,你可以选择打开虚拟滚动或者关闭
该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完善,需要逐步添加
但新表格的源码有点多,可能需要单独写一篇了,这里不详细介绍了,其实我标了注释了,只要认真看一下也知道新组件的作用的什么的
这里也感谢你可以看到这样,下次见了,拜拜!