虚拟列表(Virtual Scrolling)的核心运作逻辑,是基于人眼视觉的局限性:我们只需要渲染当前视口(Viewport)内能看到的元素,加上视口上下少量的缓冲元素。其余成千上万的数据,只需在内存中保留状态,不需要生成实际的 DOM 节点。这直接规避了浏览器因同时回流/重绘海量 DOM 而导致的性能崩溃。
实现一个基础的定高虚拟列表,需要三个关键层级:
-
视口容器(Viewport):固定高度,设置
overflow-y: auto产生滚动条。 -
占位容器(Phantom):负责撑开高度,其高度等于 总数据量 单项高度,让原生滚动条的滑块比例和滚动行为表现正常。
-
真实渲染区(Actual Content):存放真实 DOM,根据滚动距离动态计算自己应该处于的偏移量(通常通过
transform: translateY),始终保持在视口可视范围内。
核心计算逻辑
假设视口高度为 ,每一项元素的固定高度为 ,当前的滚动距离为 :
-
视口可见数量:
-
起始索引:
-
结束索引:
-
渲染区偏移量:为了让真实 DOM 始终在视野中,渲染区需要向下平移,偏移量通常等于被隐藏到上方的元素总高度:
为了防止快速滚动时的白屏闪烁,实际工程中通常会在上下各加一个缓冲区(Buffer),即略微调小 并调大 。
React 实现示例
下面是一个用 React 实现的基础定高虚拟列表组件:
import React, { useState, useRef, UIEvent, useMemo } from 'react';
interface VirtualListProps {
listData: any[]; // 总数据源
itemHeight: number; // 列表项固定高度
height: number; // 视口高度
buffer?: number; // 上下缓冲项数
}
export const VirtualList: React.FC<VirtualListProps> = ({
listData,
itemHeight,
height,
buffer = 5
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
// 1. 列表总高度 (用于撑开滚动条)
const totalHeight = listData.length * itemHeight;
// 2. 计算可见区域的数据索引
const visibleCount = Math.ceil(height / itemHeight);
// 计算起止索引 (加入缓冲区,并处理边界)
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
const endIndex = Math.min(listData.length, startIndex + visibleCount + buffer * 2);
// 3. 截取需要真正渲染的数据
const visibleData = useMemo(() => {
return listData.slice(startIndex, endIndex);
}, [listData, startIndex, endIndex]);
// 4. 计算真实渲染区的偏移量
// 偏移量由当前起始索引决定,这样能保证渲染的元素正好落在视口范围内
const startOffset = startIndex * itemHeight;
// 滚动事件处理
const handleScroll = (e: UIEvent<HTMLDivElement>) => {
// 使用 requestAnimationFrame 可以进一步优化性能,这里为了直观直接赋值
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div
ref={containerRef}
style={{ height: `${height}px`, overflow: 'auto', position: 'relative' }}
onScroll= {handleScroll}
>
{/* 占位元素:不可见,仅用于撑开完整的滚动高度 */}
<div style={{ height: `${totalHeight}px`, position: 'absolute', left: 0, top: 0, right: 0, zIndex: -1 }} />
{/* 真实渲染区:随着滚动动态改变自身垂直偏移 */}
<div style={{ transform: `translate3d(0, ${startOffset}px, 0)` }}>
{visibleData.map((item, index) => (
<div
key={startIndex + index} // 实际开发中最好用 item 自身的唯一 id
style={{ height: `${itemHeight}px`, boxSizing: 'border-box', borderBottom: '1px solid #eee' }}
>
{/* 渲染你的具体列表项 */}
Item {startIndex + index}: {item}
</div>
))}
</div>
</div>
);
};
关键优化细节
-
事件节流与重绘控制:在
onScroll事件中,频繁调用setState会触发大量重渲染。实际应用中,可以通过requestAnimationFrame将滚动状态的更新与浏览器的绘制频率对齐,或者给滚动处理函数加上节流(Throttle)。 -
translate3d开启硬件加速:使用transform: translate3d(0, ${offset}px, 0)而非修改top值,可以将重绘工作交给 GPU,避免触发浏览器的重排(Reflow)。
定高列表的数学模型相对简单。但在实际业务中,我们经常会遇到列表项内部内容长短不一,导致高度不固定的情况。
需要我进一步为你拆解**不定高虚拟列表(动态高度)**的实现方案与计算原理吗?