为什么需要虚拟滚动列表?
前端虚拟滚动技术是一种通过优化滚动列表的性能来提高Web应用程序性能的技术。在传统的滚动列表中,当用户滚动列表时,浏览器需要渲染所有的列表项,即使它们在屏幕外也需要渲染,这会导致性能问题。但是,使用虚拟滚动技术,只有可见的列表项才会被渲染,而其他列表项则在滚动时根据需要动态加载。
虚拟滚动技术通常使用一些技术来实现,例如在滚动时动态加载列表项,使用样式来隐藏屏幕外的列表项,以及在滚动停止时重新计算要渲染的列表项。虚拟滚动技术可以显著提高Web应用程序的性能和响应性,并在需要处理大量数据的情况下特别有用,例如社交媒体、电子商务等应用程序。
具体实现步骤
-
数据准备:首先,该示例使用
data数组模拟包含 200000 个列表项的长列表,每个列表项包含id和text两个字段。ITEM_HEIGHT和CONTAINER_HEIGHT分别表示每个列表项的高度和可见区域的高度。 -
状态管理:使用
useState钩子管理列表项的起始索引,即可见列表项的第一个索引。初始值为 0。使用useRef钩子引用可见区域的容器元素。 -
计算可见列表项:使用
Math.ceil函数计算可见列表项的数量,并使用slice函数从data数组中获取可见列表项的数组。每个列表项的高度为ITEM_HEIGHT,因此可见区域的高度除以每个列表项的高度即为可见列表项的数量。 -
处理滚动事件:通过
onScroll事件监听容器元素的滚动事件,并根据滚动位置计算可见列表项的起始索引。具体地,使用scrollTop属性获取容器元素的滚动位置,然后将滚动位置除以每个列表项的高度并取整数部分,即可得到可见列表项的第一个索引。将该索引设置为起始索引即可。 -
渲染列表项:使用
map函数遍历可见列表项的数组,渲染每个列表项的文本。为了占据正确的位置,对于未渲染的列表项,使用paddingTop样式填充空白区域。这里将paddingTop设置为起始索引乘以每个列表项的高度,即paddingTop: startIndex * ITEM_HEIGHT。
具体代码
import React, { useState, useRef } from "react";
import "./style.css";
interface Item {
id: number;
text: number;
}
const data: Item[] = new Array(200000).fill(0).map((_, index) => ({
id: index,
text: Math.random() + index,
}));
const ITEM_HEIGHT = 50;
const CONTAINER_HEIGHT = 300;
export default function App() {
const [startIndex, setStartIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const visibleItemCount = Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT);
const endIndex = startIndex + visibleItemCount;
const visibleData = data.slice(startIndex, endIndex);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop } = e.currentTarget;
const index = Math.floor(scrollTop / ITEM_HEIGHT);
setStartIndex(index);
};
return (
<div
className="container"
style={{ height: CONTAINER_HEIGHT, width: "100%" }}
ref={containerRef}
onScroll={handleScroll}
>
{/* 通过 paddingTop 填充空白区域 */}
<div
style={{
height: data.length * ITEM_HEIGHT,
paddingTop: startIndex * ITEM_HEIGHT,
}}
>
{visibleData.map((item, index) => (
<div
key={item.id}
style={{
height: ITEM_HEIGHT,
backgroundColor: index % 2 === 0 ? "blue" : "green",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
{item.text}
</div>
))}
</div>
</div>
);
}
样式部分
.container {
border: 1px solid #ccc;
overflow-y: scroll;
}
.item {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
border-bottom: 1px solid #ccc;
}
实现原理图
改进
1.以上代码关键的滚动区域需要加上 boxSizing: 'border-box'
2.计算后的starIndex不能无限增大,限制大小为
if(index + visibleItemCount <= data.length) {
setStartIndex(index);
}
3.防止底部出现空白的现象,增加底部buffer
let endIndex = startIndex + visibleItemCount;
if(endIndex + 1 <= data.length) {
endIndex += 1
}
组件化
提取出核心逻辑,组件化后可以供其他人使用
import React, { useState, useRef } from "react";
import "./style.css";
interface Item {
id: number;
text: number;
}
const data: Item[] = new Array(200).fill(0).map((_, index) => ({
id: index,
text: Math.random() + index,
}));
const ITEM_HEIGHT = 50;
const CONTAINER_HEIGHT = 300;
interface VirtualScrollProps {
data: Item[];
itemHeight: number;
containerHeight: number;
}
const VirtualScroll: React.FC<VirtualScrollProps> = ({
data,
itemHeight,
containerHeight,
}) => {
const [startIndex, setStartIndex] = useState(0);
const visibleItemCount = Math.ceil(containerHeight / itemHeight);
let endIndex = startIndex + visibleItemCount;
if (endIndex + 1 <= data.length) {
endIndex += 1;
}
let visibleData = data.slice(startIndex, endIndex);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { scrollTop } = e.currentTarget;
const index = Math.floor(scrollTop / itemHeight);
if (index + visibleItemCount <= data.length) {
setStartIndex(index);
}
};
return (
<div
className="virtual-scroll-container"
style={{ height: containerHeight}}
onScroll={handleScroll}
>
<div
className="virtual-scroll-content"
style={{
height: data.length * itemHeight,
paddingTop: startIndex * itemHeight,
boxSizing: "border-box",
}}
>
{visibleData.map((item, index) => (
<div
className="virtual-scroll-item"
key={item.id}
style={{
height: itemHeight,
backgroundColor: index % 2 === 0 ? "blue" : "green",
}}
>
<span>{item.text}</span>
</div>
))}
</div>
</div>
);
};
export default function App() {
return (
<VirtualScroll
data={data}
itemHeight={ITEM_HEIGHT}
containerHeight={CONTAINER_HEIGHT}
/>
);
}