虚拟滚动的核心思路是只渲染当前可视区域内的列表项,而非一次性渲染所有数据,通过动态计算滚动位置来更新可视区域的内容,从而减少DOM节点数量,提升页面渲染性能。

虚拟滚动的核心原理
虚拟滚动的实现依赖三个核心概念:
- 容器高度:列表外部容器的固定高度,用于计算可视区域的范围
- 项高度:每个列表项的高度,需要提前确定或者动态计算
- 滚动偏移:当前滚动条的位置,用来判断哪些列表项需要被渲染
整体流程可以概括为:先根据容器高度和项高度计算可视区域能展示的列表项数量,再根据滚动偏移计算起始渲染索引,最后只渲染对应区间的列表项,同时用占位元素撑起整个列表的高度,保证滚动条行为正常。
基础虚拟滚动组件实现
1. 组件结构设计
我们先搭建组件的基础结构,包含外层容器、占位容器和内容渲染区域三个部分:
<div class="virtual-list-container" id="virtualList"> <!-- 占位容器,用于撑起整个列表的高度 --> <div class="virtual-list-phantom" id="phantom"></div> <!-- 实际渲染内容的容器 --> <div class="virtual-list-content" id="content"></div> </div>
2. 样式设置
外层容器需要设置固定高度和溢出滚动,占位容器和内容容器采用绝对定位布局:
.virtual-list-container {
height: 500px;
overflow-y: auto;
position: relative;
border: 1px solid #e5e5e5;
}
.virtual-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
3. 核心逻辑实现
接下来实现JavaScript核心逻辑,包含初始化、滚动事件处理、内容更新三个部分:
// 虚拟滚动列表类
class VirtualList {
constructor(options) {
// 容器元素
this.container = options.container;
// 列表数据
this.data = options.data || [];
// 每个列表项的高度
this.itemHeight = options.itemHeight || 50;
// 额外渲染的缓冲区数量,避免快速滚动时出现空白
this.buffer = options.buffer || 5;
// 内部元素引用
this.phantom = this.container.querySelector('.virtual-list-phantom');
this.content = this.container.querySelector('.virtual-list-content');
// 初始化
this.init();
}
init() {
// 设置占位容器高度,撑起整个列表
this.phantom.style.height = this.data.length * this.itemHeight + 'px';
// 绑定滚动事件
this.container.addEventListener('scroll', this.handleScroll.bind(this));
// 首次渲染
this.updateRender();
}
// 处理滚动事件
handleScroll() {
// 使用requestAnimationFrame避免频繁触发重绘
requestAnimationFrame(() => {
this.updateRender();
});
}
// 更新渲染内容
updateRender() {
// 获取当前滚动距离
const scrollTop = this.container.scrollTop;
// 计算可视区域能展示的项数
const visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight);
// 计算起始索引,加上缓冲区
const startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
// 计算结束索引,加上缓冲区
const endIndex = Math.min(this.data.length, startIndex + visibleCount + 2 * this.buffer);
// 计算内容区域的偏移量
const offsetY = startIndex * this.itemHeight;
this.content.style.transform = `translateY(${offsetY}px)`;
// 渲染对应区间的列表项
this.renderItems(startIndex, endIndex);
}
// 渲染列表项
renderItems(startIndex, endIndex) {
const fragment = document.createDocumentFragment();
for (let i = startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.className = 'virtual-list-item';
item.style.height = this.itemHeight + 'px';
item.textContent = this.data[i];
fragment.appendChild(item);
}
// 清空原有内容再添加新内容,避免多次重绘
this.content.innerHTML = '';
this.content.appendChild(fragment);
}
}
// 初始化示例
const data = Array.from({ length: 10000 }, (_, i) => `列表项 ${i + 1}`);
const virtualList = new VirtualList({
container: document.getElementById('virtualList'),
data: data,
itemHeight: 50
});
性能优化点说明
上述实现已经能满足基础的高性能虚拟滚动需求,还可以从以下几个方面进一步优化:
- 使用文档片段:渲染列表项时使用
DocumentFragment,减少DOM操作次数,避免多次触发重排 - 滚动事件节流:虽然用了
requestAnimationFrame,但如果是高频滚动场景,可以结合节流函数进一步控制触发频率 - 动态项高度适配:如果列表项高度不固定,可以提前缓存每个项的高度,或者在渲染后动态测量高度并更新占位容器
- 复用列表项节点:可以维护一个节点池,滚动时复用已有的DOM节点,避免频繁创建和销毁节点
注意事项
实现虚拟滚动时需要注意,项高度必须可计算,否则无法准确计算滚动偏移和占位高度。如果是动态高度场景,需要额外维护每个项的高度缓存,初始渲染时可以先按预估高度计算,渲染完成后测量实际高度再更新缓存和占位容器。
另外,滚动事件的处理要尽量避免复杂的计算,把非必要的逻辑移出滚动回调,防止阻塞主线程导致页面卡顿。