在前端开发中,当一次性渲染大量数据时,直接渲染所有DOM节点,会造成渲染过慢,浏览器卡顿的现象,导致用户体验不佳,为了改善这种情况,提出使用虚拟列表的方式进行渲染。
虚拟列表的实现思路
1.只渲染可见区域:
计算当前可见区域的起始索引和结束索引。
只渲染可见区域内的列表项,其他区域用空白占位。
2.动态计算高度:
如果列表项高度固定,可以直接计算。
如果列表项高度不固定,需要动态计算每个列表项的高度。
3.滚动时更新渲染:
监听滚动事件,动态更新可见区域的列表项。
实现步骤
以下是实现虚拟列表的关键步骤:
- 固定高度的虚拟列表
假设列表项高度固定,实现较为简单。 - 动态高度的虚拟列表
列表项高度不固定,需要动态计算。
代码实现
固定高度的虚拟列表
以下是一个固定高度的虚拟列表实现:
css
import React, { useState, useRef, useCallback } from "react";
const VirtualList = ({ data, itemHeight, visibleCount }) => {
const [startIndex, setStartIndex] = useState(0); // 起始索引
const containerRef = useRef(null); // 容器引用
// 计算可见区域的数据
const visibleData = data.slice(startIndex, startIndex + visibleCount);
// 处理滚动事件
const handleScroll = useCallback(() => {
if (containerRef.current) {
const scrollTop = containerRef.current.scrollTop;
const newStartIndex = Math.floor(scrollTop / itemHeight);
setStartIndex(newStartIndex);
}
}, [itemHeight]);
return (
<div
ref={containerRef}
style={{
height: `${visibleCount * itemHeight}px`,
overflow: "auto",
border: "1px solid #ccc",
}}
onScroll={handleScroll}
>
<div
style={{
height: `${data.length * itemHeight}px`,
position: "relative",
}}
>
{visibleData.map((item, index) => (
<div
key={startIndex + index}
style={{
position: "absolute",
top: `${(startIndex + index) * itemHeight}px`,
width: "100%",
height: `${itemHeight}px`,
boxSizing: "border-box",
borderBottom: "1px solid #eee",
}}
>
{item}
</div>
))}
</div>
</div>
);
};
export default VirtualList;
使用示例
css
import React from "react";
import VirtualList from "./VirtualList";
const App = () => {
const data = Array.from({ length: 10000 }, (_, index) => `Item ${index + 1}`);
return (
<div>
<h1>Virtual List Example</h1>
<VirtualList data={data} itemHeight={50} visibleCount={10} />
</div>
);
};
export default App;
动态高度的虚拟列表
如果列表项高度不固定,需要动态计算高度并缓存。以下是一个简单的实现思路:
初始化时测量高度:
渲染一个隐藏的列表项,测量其高度并缓存。
滚动时动态更新:
根据缓存的高度计算可见区域的起始索引和结束索引。
css
import React, { useState, useRef, useCallback, useEffect } from "react";
const DynamicVirtualList = ({ data, visibleCount }) => {
const [startIndex, setStartIndex] = useState(0);
const [heights, setHeights] = useState([]); // 缓存高度
const containerRef = useRef(null);
const itemRefs = useRef([]); // 列表项引用
// 初始化时测量高度
useEffect(() => {
const newHeights = itemRefs.current.map(
(ref) => ref?.getBoundingClientRect().height || 0
);
setHeights(newHeights);
}, [data]);
// 计算可见区域的数据
const visibleData = data.slice(startIndex, startIndex + visibleCount);
// 计算总高度
const totalHeight = heights.reduce((sum, height) => sum + height, 0);
// 计算起始偏移量
const offset = heights.slice(0, startIndex).reduce((sum, height) => sum + height, 0);
// 处理滚动事件
const handleScroll = useCallback(() => {
if (containerRef.current) {
const scrollTop = containerRef.current.scrollTop;
let newStartIndex = 0;
let sum = 0;
while (sum + heights[newStartIndex] < scrollTop) {
sum += heights[newStartIndex];
newStartIndex++;
}
setStartIndex(newStartIndex);
}
}, [heights]);
return (
<div
ref={containerRef}
style={{
height: "500px",
overflow: "auto",
border: "1px solid #ccc",
}}
onScroll={handleScroll}
>
<div style={{ height: `${totalHeight}px`, position: "relative" }}>
{visibleData.map((item, index) => (
<div
key={startIndex + index}
ref={(el) => (itemRefs.current[startIndex + index] = el)}
style={{
position: "absolute",
top: `${offset}px`,
width: "100%",
boxSizing: "border-box",
borderBottom: "1px solid #eee",
}}
>
{item}
</div>
))}
</div>
</div>
);
};
export default DynamicVirtualList;
使用示例
css
import React from "react";
import DynamicVirtualList from "./DynamicVirtualList";
const App = () => {
const data = Array.from({ length: 10000 }, (_, index) => `Item ${index + 1}`);
return (
<div>
<h1>Dynamic Virtual List Example</h1>
<DynamicVirtualList data={data} visibleCount={10} />
</div>
);
};
export default App;