你是不是也遇到过这种场景?产品经理兴冲冲跑过来说:"这个页面要展示1万条数据,用户体验一定要流畅哦!"结果你吭哧吭哧写完,页面一加载......直接卡成PPT,滚动起来比看幻灯片还刺激。
别慌!今天我就带你彻底搞懂虚拟列表这个神器,让你轻松驾驭万级数据渲染,从此告别卡顿烦恼!
什么是虚拟列表?为什么能这么牛?
简单来说,虚拟列表就是个"聪明偷懒"的渲染方案。它不会傻乎乎地把所有数据都塞进DOM里,而是只渲染你看得见的那部分内容。
想象一下,你有个能装1000个item的列表,但屏幕一次只能显示10个。传统做法是把1000个都渲染出来,而虚拟列表只会渲染当前看得见的10个,外加上下各预留几个做缓冲。当你滚动时,它就像个魔术师一样快速切换显示的内容。
这样做的好处简直不要太明显:DOM节点数量暴降,内存占用大幅减少,滚动流畅得飞起!实测下来,万级数据列表也能做到60fps丝滑滚动。
手把手教你用现成轮子(react-window)
自己造轮子太累?咱们先来看看业界神器react-window怎么用。三行代码就能搞定基础需求,简单到哭!
jsx
import { FixedSizeList as List } from 'react-window';
// 最简单的虚拟列表实现
const MyVirtualList = () => (
<List
height={400} // 列表容器高度
itemCount={10000} // 总条目数
itemSize={50} // 每个条目高度(固定高度时用)
width={300} // 列表宽度
>
{({ index, style }) => (
<div style={style}>
我是第 {index} 条数据
</div>
)}
</List>
);
看明白了吗?FixedSizeList适合所有item高度固定的场景。那个style参数特别重要,它会给每个item注入绝对定位的样式,让它们乖乖呆在正确的位置上。
如果你的item高度不固定,别慌,用VariableSizeList就行:
jsx
import { VariableSizeList as List } from 'react-window';
const rowHeights = new Array(1000)
.fill(true)
.map(() => 25 + Math.round(Math.random() * 50));
const getItemSize = index => rowHeights[index];
const MyVariableList = () => (
<List
height={400}
itemCount={1000}
itemSize={getItemSize} // 传入高度计算函数
width={300}
>
{({ index, style }) => (
<div style={style}>
我是高度不固定的第 {index} 条
</div>
)}
</List>
);
自己动手实现?这些坑我帮你踩过了
虽然用库很方便,但了解原理很重要。自己实现一个基础版的虚拟列表,能帮你彻底搞懂它的工作原理。
jsx
import React, { useState, useRef, useMemo } from 'react';
const MyOwnVirtualList = ({ items, itemHeight, containerHeight }) => {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);
// 计算可见区域的内容
const visibleItems = useMemo(() => {
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
items.length - 1,
startIndex + Math.ceil(containerHeight / itemHeight)
);
return items.slice(startIndex, endIndex + 1);
}, [scrollTop, items, itemHeight, containerHeight]);
// 计算内层容器的总高度(撑开滚动条)
const innerContainerHeight = items.length * itemHeight;
const handleScroll = (e) => {
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div
ref={containerRef}
style={{
height: containerHeight,
overflow: 'auto',
border: '1px solid #ddd'
}}
onScroll={handleScroll}
>
{/* 这个div用来撑开滚动条 */}
<div style={{ height: innerContainerHeight }}>
{/* 可见区域的内容 */}
<div
style={{
position: 'relative',
height: `${visibleItems.length * itemHeight}px`,
top: `${Math.floor(scrollTop / itemHeight) * itemHeight}px`
}}
>
{visibleItems.map((item, index) => (
<div
key={index}
style={{
position: 'absolute',
top: `${index * itemHeight}px`,
height: `${itemHeight}px`,
width: '100%'
}}
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
};
// 使用示例
const App = () => {
const mockData = Array.from({ length: 10000 }, (_, i) => ({
id: i,
content: `列表项 ${i}`
}));
return (
<MyOwnVirtualList
items={mockData}
itemHeight={50}
containerHeight={400}
/>
);
};
这个自实现版本虽然简单,但已经包含了虚拟列表的核心思想:计算可见区域、动态渲染、用绝对定位控制位置。
自己实现时最容易踩的坑:
- 忘了给容器设置overflow: auto,结果根本滚不动
- 滚动时出现空白,因为没及时更新可见区域的计算
- 滚动条抖动,因为高度计算有误差
- 快速滚动时内容闪烁,因为渲染速度跟不上滚动速度
实战技巧:让虚拟列表更强大
光会基础用法还不够,这些实战技巧能让你的虚拟列表更专业:
- 滚动节流:避免滚动时过于频繁地更新渲染
jsx
// 用lodash的throttle或者自己实现
const throttledScroll = useMemo(
() => throttle(handleScroll, 16), // 约60fps
[]
);
- 预渲染几屏内容:避免快速滚动时出现白屏
jsx
// 在计算可见区域时,上下多算一些
const buffer = 5; // 多渲染5条
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
const endIndex = Math.min(
items.length - 1,
startIndex + Math.ceil(containerHeight / itemHeight) + buffer
);
- 记住滚动位置:页面切换回来时保持之前的位置
jsx
// 用useRef记住位置,或者存到localStorage
const savedScrollTop = useRef(0);
const handleScroll = (e) => {
savedScrollTop.current = e.currentTarget.scrollTop;
// ...其他逻辑
};
总结:什么时候该用虚拟列表?
虚拟列表虽好,但也不是万能药。我总结了个简单决策流程:
数据量小于100条?→ 直接正常渲染,别折腾 数据量100-1000条?→ 可以考虑分页加载 数据量1000条以上?→ 果断上虚拟列表!
记住,虚拟列表最适合的是那种"长得没边"的列表。如果是复杂表格或者需要频繁操作的数据,可能需要考虑其他方案。
如果觉得这篇文章有帮助,记得点赞收藏,下次遇到性能问题就不慌啦~