固高虚拟列表实现
核心思路
这个虚拟列表的核心思想是只渲染可视区域内的元素,而不是渲染所有数据项。通过动态计算和更新 DOM 来实现高性能的长列表展示。
关键步骤分解
1. 初始化阶段
- 容器设置:创建固定高度的容器(600px),设置 overflow-y: auto 启用滚动
- 状态管理:维护关键状态数据
- dataSource: 数据源数组
- itemHeight: 每个列表项的固定高度(100px)
- viewHeight: 可视区域高度
- maxCount: 可视区域最多显示的项目数量
2. 核心计算逻辑
- 起始索引计算:startIndex = Math.floor(scrollTop / itemHeight)
- 结束索引计算:endIndex = startIndex + maxCount
- 渲染列表切片:renderList = dataSource.slice(startIndex, endIndex)
3. 视觉欺骗技术
- 总高度模拟:设置列表容器高度为 dataSource.length * itemHeight,让滚动条显示正确的总长度
- 位置偏移:使用 transform: translate3d(0, ${startIndex * itemHeight}px, 0) 将可见元素定位到正确位置
4. 性能优化策略
- RAF 节流:使用 requestAnimationFrame 对滚动事件进行节流处理
- 变化检测:只有当 startIndex 发生变化时才重新渲染
- 懒加载:当滚动接近底部时自动加载更多数据
5. 事件处理流程
滚动事件触发 → 计算 startIndex → 检查是否变化 → 重新渲染 → 更新 DOM
实现亮点
- 固定高度优化:每个项目高度固定为 100px,简化了计算逻辑
- 缓冲区设计:maxCount 比实际可见数量多 1 个,提供缓冲避免滚动时的闪烁
- 自动扩容:当滚动到接近底部时自动添加新数据
- GPU 加速:使用 translate3d 启用硬件加速
适用场景
- 大量数据的列表展示(如商品列表、聊天记录等)
- 每个列表项高度相同的场景
- 需要流畅滚动体验的长列表
这个实现是一个经典的固定高度虚拟列表方案,代码简洁且性能良好。对于更复杂的场景(如动态高度),需要额外的高度缓存和计算逻辑。
完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* 外层容器 - 定义虚拟列表的整体尺寸 */
.container {
width: 600px;
height: 600px;
margin: 100px auto;
border: 1px solid red;
}
/* 滚动容器 - 提供滚动能力,触发滚动事件 */
.fs-virtuallist-container {
width: 100%;
height: 100%;
overflow-y: auto; /* 启用垂直滚动 */
}
/* 列表容器 - 承载实际的列表项,通过 transform 控制位置 */
.fs-virtuallist-list {
width: 100%;
}
/* 列表项样式 - 固定高度是虚拟列表计算的基础 */
.fs-virtuallist-item {
width: 100%;
height: 100px; /* 固定高度,简化计算逻辑 */
box-sizing: border-box;
border: 1px solid #000;
text-align: center;
font-size: 20px;
line-height: 100px; /* 垂直居中 */
}
</style>
</head>
<body>
<!-- 虚拟列表 DOM 结构 -->
<div class="container">
<!-- 滚动容器:监听滚动事件,提供可视区域 -->
<div class="fs-virtuallist-container">
<!-- 列表容器:通过 JS 动态设置高度和位置,实现虚拟滚动效果 -->
<div class="fs-virtuallist-list">
<!-- 列表项将通过 JavaScript 动态生成 -->
</div>
</div>
</div>
<script>
/**
* 虚拟列表类 - 只渲染可视区域内的元素,提升长列表性能
*/
class FsVirtuallist {
constructor(containerSelector, listSelector) {
// 核心状态数据
this.state = {
dataSource: [], // 数据源数组
itemHeight: 100, // 每个列表项的固定高度
viewHeight: 0, // 可视区域高度
maxCount: 0, // 可视区域最多显示的项目数量
};
// 滚动样式对象,用于控制列表位置和高度
this.scrollStyle = {};
// 渲染范围索引
this.startIndex = 0; // 当前渲染的起始索引
this.endIndex = 0; // 当前渲染的结束索引
this.lastStart = -1; // 上次渲染的起始索引,用于变化检测
// 当前需要渲染的数据切片
this.renderList = [];
// DOM 元素引用
this.oContainer = document.querySelector(containerSelector); // 滚动容器
this.oList = document.querySelector(listSelector); // 列表容器
}
/**
* 初始化虚拟列表
*/
init() {
// 计算可视区域高度
this.state.viewHeight = this.oContainer.offsetHeight;
// 计算可视区域最多显示的项目数量(+1 作为缓冲区,避免滚动时闪烁)
this.state.maxCount = Math.ceil(this.state.viewHeight / this.state.itemHeight) + 1;
// 绑定滚动事件
this.bindEvent();
// 初始化数据
this.addData();
// 首次渲染
this.render();
}
/**
* 绑定滚动事件,使用 RAF 节流优化性能
*/
bindEvent() {
this.oContainer.addEventListener("scroll", this.rafThrottle(this.handleScroll.bind(this)));
}
/**
* 计算渲染结束索引
*/
computedEndIndex() {
const end = this.startIndex + this.state.maxCount;
// 确保结束索引不超过数据源长度
this.endIndex = this.state.dataSource[end] ? end : this.state.dataSource.length;
// 懒加载:当滚动接近底部时自动加载更多数据
if (this.endIndex >= this.state.dataSource.length) {
this.addData();
}
}
/**
* 计算当前需要渲染的数据切片
*/
computedRenderList() {
this.renderList = this.state.dataSource.slice(this.startIndex, this.endIndex);
}
/**
* 计算滚动样式 - 实现视觉欺骗的关键
*/
computedScrollStyle() {
const { dataSource, itemHeight } = this.state;
this.scrollStyle = {
// 设置列表总高度,减去已滚动过的高度,让滚动条显示正确
height: `${dataSource.length * itemHeight - this.startIndex * itemHeight}px`,
// 使用 transform 将可见元素定位到正确位置(GPU 加速)
transform: `translate3d(0, ${this.startIndex * itemHeight}px, 0)`,
};
}
/**
* 滚动事件处理函数
*/
handleScroll() {
const { scrollTop } = this.oContainer;
// 根据滚动距离计算起始索引
this.startIndex = Math.floor(scrollTop / this.state.itemHeight);
// 只有当起始索引发生变化时才重新渲染(性能优化)
if (this.startIndex !== this.lastStart) this.render();
// 记录本次起始索引,用于下次比较
this.lastStart = this.startIndex;
}
/**
* 渲染函数 - 虚拟列表的核心渲染逻辑
*/
render() {
// 1. 计算渲染范围
this.computedEndIndex();
// 2. 获取需要渲染的数据切片
this.computedRenderList();
// 3. 计算滚动样式
this.computedScrollStyle();
// 4. 生成 HTML 模板
const template = this.renderList.map(i => `<div class="fs-virtuallist-item">${i}</div>`).join("");
// 5. 更新 DOM
const { height, transform } = this.scrollStyle;
this.oList.innerHTML = template; // 更新列表内容
this.oList.style.height = height; // 设置列表高度
this.oList.style.transform = transform; // 设置位置偏移
}
/**
* 添加数据 - 模拟数据加载
*/
addData() {
// 每次添加 10 条数据
for (let i = 0; i < 10; i++) {
this.state.dataSource.push(this.state.dataSource.length + 1);
}
}
/**
* RAF 节流函数 - 优化滚动性能
* @param {Function} fn 需要节流的函数
* @returns {Function} 节流后的函数
*/
rafThrottle(fn) {
let lock = false;
return function (...args) {
window.requestAnimationFrame(() => {
// 如果上一帧还在处理中,跳过本次执行
if (lock) return;
lock = true;
fn.apply(this, args);
lock = false;
});
};
}
}
// 创建虚拟列表实例并初始化
const vList = new FsVirtuallist(".fs-virtuallist-container", ".fs-virtuallist-list");
vList.init();
</script>
</body>
</html>