虚拟列表组件用于高效渲染大量数据。通过只渲染可视区域内的项目,可以轻松处理数万甚至数十万条数据而不影响性能。
当所有项目高度相同时,使用定高模式可以获得最佳性能。
import { VirtualList } from '@enterprise-ui/react19';
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
<VirtualList
items={items}
height={400}
itemHeight={60} // 固定高度
renderItem={(item) => (
<div className="p-4 border-b">
{item.name}
</div>
)}
/>当项目高度不一致时,使用不定高模式。组件会自动测量和缓存每个项目的高度。
import { VirtualList } from '@enterprise-ui/react19';
const items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: '描述文字',
}));
<VirtualList
items={items}
height={400}
estimatedItemHeight={80} // 预估高度
renderItem={(item) => (
<div className="p-4 border-b">
<div className="font-medium">{item.name}</div>
<div className="text-sm">{item.description}</div>
</div>
)}
/>可以通过 scrollToIndex 属性滚动到指定索引。
const [scrollToIndex, setScrollToIndex] = useState<number>();
<VirtualList
items={items}
height={300}
itemHeight={50}
scrollToIndex={scrollToIndex}
renderItem={(item) => <div>{item.name}</div>}
/>
<button onClick={() => setScrollToIndex(5000)}>
滚动到第 5000 项
</button>定高模式是最简单高效的虚拟列表实现方式。当所有项目高度相同时,可以直接通过数学计算确定可视区域。
// 1. 计算可视区域起始索引
const startIndex = Math.floor(scrollTop / itemHeight);
// 2. 计算可视区域结束索引
const visibleCount = Math.ceil(containerHeight / itemHeight);
const endIndex = Math.min(
startIndex + visibleCount + overscan * 2,
totalItems - 1
);
// 3. 计算总高度和偏移量
const totalHeight = totalItems * itemHeight;
const offsetY = startIndex * itemHeight;
// 4. 只渲染可见项
const visibleItems = items.slice(startIndex, endIndex + 1);transform: translateY() 而非 top,利用 GPU 加速overscan 个项目,减少滚动时的空白不定高模式更复杂,因为需要动态测量每个项目的高度。核心思路是:高度缓存 + 位置计算 + 二分查找。
// 高度缓存 Map<index, height>
const heightCache = new Map<number, number>();
// 位置缓存 Map<index, offset>
const offsetCache = new Map<number, number>();// 1. 计算累计高度(位置)
function getItemOffset(index: number): number {
// 如果缓存中有,直接返回
if (offsetCache.has(index)) {
return offsetCache.get(index)!;
}
// 否则计算累计高度
let offset = 0;
for (let i = 0; i < index; i++) {
offset += heightCache.get(i) || estimatedHeight;
}
// 缓存结果
offsetCache.set(index, offset);
return offset;
}
// 2. 二分查找起始索引(优化查找)
function findStartIndex(scrollTop: number): number {
let left = 0;
let right = items.length - 1;
let result = 0;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const offset = getItemOffset(mid);
if (offset < scrollTop) {
left = mid + 1;
result = mid;
} else {
right = mid - 1;
}
}
return Math.max(0, result - overscan);
}
// 3. 查找结束索引
function findEndIndex(startIndex: number, scrollTop: number, containerHeight: number): number {
const startOffset = getItemOffset(startIndex);
let currentOffset = startOffset;
let index = startIndex;
while (index < items.length && currentOffset < scrollTop + containerHeight) {
const itemHeight = heightCache.get(index) || estimatedHeight;
currentOffset += itemHeight;
index++;
}
return Math.min(items.length - 1, index + overscan);
}// 高度测量实现
useEffect(() => {
visibleItems.forEach(({ index }) => {
const element = itemRefs.current.get(index);
if (element && !heightCache.has(index)) {
// 测量实际高度
const measuredHeight = element.offsetHeight;
heightCache.set(index, measuredHeight);
// 清除位置缓存(因为高度变化会影响后续位置)
clearOffsetCacheFrom(index);
}
});
}, [visibleItems]);| 模式 | 时间复杂度 | 空间复杂度 | 首次渲染 | 滚动性能 |
|---|---|---|---|---|
| 定高模式 | O(1) | O(visibleCount) | 极快 | 60fps |
| 不定高模式 | O(log n) | O(n) 缓存 | 需要测量 | 60fps(缓存后) |
// 使用 requestAnimationFrame 优化滚动
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const newScrollTop = e.currentTarget.scrollTop;
// 使用 requestAnimationFrame 确保在下一帧更新
requestAnimationFrame(() => {
setScrollTop(newScrollTop);
onScroll?.(newScrollTop);
});
};transform 而非改变 top/left,避免重排overscan 个项目,减少滚动时的闪烁| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| items | 数据列表 | T[] | - |
| renderItem | 渲染函数 | (item: T, index: number) => React.ReactNode | - |
| height | 容器高度 | number | - |
| itemHeight | 每项高度(定高模式) | number | - |
| estimatedItemHeight | 预估高度(不定高模式) | number | 50 |
| overscan | 缓冲区大小 | number | 5 |
| scrollToIndex | 滚动到指定索引 | number | - |
| onScroll | 滚动事件回调 | (scrollTop: number) => void | - |
| className | 自定义类名 | string | - |
overscan 值,平衡性能和体验'use client',需要在客户端组件中使用