引言
处理大量数据的列表渲染是一个常见的性能瓶颈。虚拟滚动(Virtual Scrolling)技术通过仅渲染可见区域的内容,显著提升了长列表的性能。本文将介绍如何使用 @tanstack/react-virtual
在同一个滚动区域内实现多个虚拟滚动列表。
利用 @tanstack/react-virtual
改造,对历史代码的改造影响范围可控,无心智负担。对一个长列表不同的模块,可以分步完成改造。 demo
里展示了两个虚拟列表,如果有更多个虚拟列表的部分,也是适用的。比传统的拼接数据到同一个列表里,此方案更加的快捷能够完成虚拟列表的改造,满足性能、体验要求。
虚拟滚动的原理
虚拟滚动的核心思想是 按需渲染。具体来说:
- 计算可见区域:根据滚动容器的滚动位置,计算出当前可见的区域。
- 动态渲染内容:仅渲染当前可见区域内的列表项,避免渲染所有数据。
- 占位空间:为未渲染的列表项保留占位空间,确保滚动条的行为与完整列表一致。
这样子可以显著减少 DOM 节点的数量,从而提升性能。
@tanstack/react-virtual 简介
@tanstack/react-virtual
是一个轻量级的虚拟滚动库,支持 React 和其他框架。它的主要特点包括:
- 支持动态高度的列表项。
- 提供灵活的 API,可以自定义滚动行为。
- 高性能,适用于大规模数据渲染。
实现多虚拟滚动列表
需求场景
假设我们需要在一个滚动容器内实现两个虚拟列表:
- 顶部列表:包含 100 个动态高度的列表项。
- 底部列表:包含 300 个动态高度的列表项。
实现步骤
1. 安装依赖
首先,安装必要的依赖:
bash
pnpm install @tanstack/react-virtual @faker-js/faker
2. 初始化数据
使用 @faker-js/faker
生成随机数据:
js
const randomNumber = (min: number, max: number) =>
faker.number.int({ min, max });
const sentences = new Array(300)
.fill(true)
.map(() => faker.lorem.sentence(randomNumber(20, 70)));
const list = new Array(100).fill(true).map(() => faker.lorem.sentence(randomNumber(20, 70)));
3. 创建滚动容器
定义一个滚动容器,并设置其高度和宽度:
js
const virtualizer = useVirtualizer({
count: sentences.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 45, // 预估高度
enabled: true,
});
const listVirtualizer = useVirtualizer({
count: list.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // 预估高度
enabled: true,
});
4. 实现虚拟滚动
使用 useVirtualizer
创建两个虚拟滚动列表:
js
const virtualizer = useVirtualizer({
count: sentences.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 45, // 预估高度
enabled: true,
});
const listVirtualizer = useVirtualizer({
count: list.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40, // 预估高度
enabled: true,
});
5. 渲染列表项
动态渲染两个列表的内容
js
<div style={{
height: virtualizer.getTotalSize() + listVirtualizer.getTotalSize(),
}}>
{/* 顶部列表 */}
<div style={{
height: listVirtualizer.getTotalSize(),
position: "relative",
}}>
{listItems.map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={listVirtualizer.measureElement}
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
style={{
position: "absolute",
transform: `translateY(${virtualRow.start}px)`,
width: "100%",
}}
>
<div style={{ padding: "10px 0" }}>
<div>Row {virtualRow.index}</div>
<div>{sentences[virtualRow.index]}</div>
</div>
</div>
))}
</div>
{/* 底部列表 */}
<div style={{
position: 'relative',
}}>
<div style={{
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
transform: `translateY(${(items[0]?.start ?? 0) - height}px)`,
}}>
{items.map((virtualRow) => (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={virtualizer.measureElement}
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
>
<div style={{ padding: "10px 0" }}>
<div>Row {virtualRow.index}</div>
<div>{sentences[virtualRow.index]}</div>
</div>
</div>
))}
</div>
</div>
</div>
项目地址
kevlin-sean.github.io/tanstack_re...
关键点
- 共享滚动容器 :两个列表共享同一个滚动容器 (
parentRef
)。 - 动态高度 :通过
measureElement
动态测量列表项的高度。 - 占位空间 :使用
getTotalSize
计算总高度,确保滚动条行为正确。