VirtualList 虚拟列表
虚拟列表组件用于高效渲染大量数据。通过只渲染可视区域内的项目,可以轻松处理数万甚至数十万条数据而不影响性能。
特性
- ✅ 支持定高和不定高两种模式
- ✅ 自动计算可视区域,只渲染可见项
- ✅ 缓冲区机制,减少滚动时的闪烁
- ✅ 不定高模式支持动态高度测量和缓存
- ✅ 支持滚动到指定索引
- ✅ SSR 兼容
定高模式
当所有项目高度相同时,使用定高模式可以获得最佳性能。
Item 0
这是第 0 项
Item 1
这是第 1 项
Item 2
这是第 2 项
Item 3
这是第 3 项
Item 4
这是第 4 项
Item 5
这是第 5 项
Item 6
这是第 6 项
Item 7
这是第 7 项
Item 8
这是第 8 项
Item 9
这是第 9 项
Item 10
这是第 10 项
Item 11
这是第 11 项
Item 12
这是第 12 项
Item 13
这是第 13 项
Item 14
这是第 14 项
Item 15
这是第 15 项
Item 16
这是第 16 项
Item 17
这是第 17 项
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>
)}
/>不定高模式
当项目高度不一致时,使用不定高模式。组件会自动测量和缓存每个项目的高度。
Item 0
这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 1
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
Item 2
这是一段描述文字
额外信息
Item 3
这是一段描述文字
额外信息
Item 4
这是一段描述文字 这是一段描述文字
额外信息
Item 5
这是一段描述文字
Item 6
这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 7
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
Item 8
这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字 这是一段描述文字
额外信息
Item 9
这是一段描述文字 这是一段描述文字 这是一段描述文字
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 属性滚动到指定索引。
Item 0
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9
Item 10
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
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. 定高模式原理
定高模式是最简单高效的虚拟列表实现方式。当所有项目高度相同时,可以直接通过数学计算确定可视区域。
核心算法:
// 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);性能分析:
- 时间复杂度:O(1) - 直接计算,无需遍历
- 空间复杂度:O(visibleCount) - 只存储可见项
- DOM 节点数:visibleCount + overscan * 2(通常 < 50 个)
- 滚动性能:60fps,无卡顿
关键优化点:
- 使用
transform: translateY()而非top,利用 GPU 加速 - 缓冲区机制:上下各渲染
overscan个项目,减少滚动时的空白 - 避免频繁的 DOM 操作,只在索引变化时更新
2. 不定高模式原理
不定高模式更复杂,因为需要动态测量每个项目的高度。核心思路是:高度缓存 + 位置计算 + 二分查找。
核心数据结构:
// 高度缓存 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);
}高度测量策略:
- 懒加载测量:只在项目进入可视区域时测量高度
- Ref 回调:使用 React ref 获取 DOM 元素的实际高度
- 缓存机制:测量后的高度存入 Map,避免重复测量
- 预估高度:未测量的项目使用预估高度,减少布局抖动
// 高度测量实现
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(log n) 时间复杂度查找起始索引
- 位置缓存:缓存累计高度,避免重复计算
- 增量更新:高度变化时只更新受影响的位置缓存
- 防抖测量:避免频繁的 DOM 测量操作
性能对比:
| 模式 | 时间复杂度 | 空间复杂度 | 首次渲染 | 滚动性能 |
|---|---|---|---|---|
| 定高模式 | O(1) | O(visibleCount) | 极快 | 60fps |
| 不定高模式 | O(log n) | O(n) 缓存 | 需要测量 | 60fps(缓存后) |
3. 滚动优化
滚动事件处理:
// 使用 requestAnimationFrame 优化滚动
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const newScrollTop = e.currentTarget.scrollTop;
// 使用 requestAnimationFrame 确保在下一帧更新
requestAnimationFrame(() => {
setScrollTop(newScrollTop);
onScroll?.(newScrollTop);
});
}, [onScroll]);避免布局抖动:
- 使用
transform而非改变top/left,避免重排 - 预估高度减少首次渲染时的布局变化
- 缓冲区机制平滑滚动体验
性能优化
- 定高模式:O(1) 时间复杂度计算位置,性能最优
- 不定高模式:使用高度缓存和二分查找,O(log n) 时间复杂度
- 缓冲区:上下各渲染
overscan个项目,减少滚动时的闪烁 - 懒加载高度:不定高模式只在需要时测量高度
API
VirtualList Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| 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',需要在客户端组件中使用