背景:为什么我们需要展示十万条数据?
在开发电商后台、订单管理系统或数据报表时,我们经常会遇到一个需求:展示超大规模的数据列表。
产品经理可能会说:“我们需要展示十万条订单数据,方便业务人员快速筛选和对比。”——这背后有几个关键的业务需求:
- 实时筛选与排序:数据量大,操作要及时反馈,不能卡顿。
- 流畅的滚动体验:滚动时不允许页面掉帧,保证用户体验。
- 额外的交互需求:可能包括图片展示、动态展开详情等。
这些需求听起来很正常,但对前端来说简直像是一场“滚动的噩梦”。想象一下,直接渲染十万个DOM节点,简直是在告诉浏览器:“给我表演个卡成PPT吧!”
第一幕:传统渲染的瓶颈
我们先来看个“憨憨”方案,一次性把十万条数据全渲染出来:
// 经典的全量渲染方法
function renderList(data) {
const container = document.getElementById('list');
container.innerHTML = ''; // 清空已有内容
data.forEach(item => {
const div = document.createElement('div');
div.className = 'item';
div.innerHTML = `
<div>订单号:${item.id}</div>
<div>金额:${item.amount}</div>
<div>时间:${new Date(item.time).toLocaleString()}</div>
`;
container.appendChild(div);
});
}
你可能觉得这没什么问题?但执行下来,浏览器直接崩溃了。
问题在哪里?
- 性能瓶颈:一次性生成十万个DOM节点,页面会变得卡到飞起。
- 内存问题:DOM节点占用内存,直接把你的浏览器推向极限。
- 滚动迟滞:滚动事件触发频繁,浏览器得反复计算布局,掉帧是家常便饭。
这就像是往一个小水桶里倒进十吨水——溢出只是时间问题。
第二幕:虚拟列表的核心思路
于是,我们得请出前端性能优化的救星——虚拟列表!
什么是虚拟列表?
虚拟列表的核心思路是:只渲染可视区域的数据,其余的统统隐藏。
想象一下,用户的视口只能看到10条数据,那我们就渲染10条,而不是一股脑全丢上去。
实现的关键步骤:
- 计算可视范围:实时确定当前视口显示的数据索引。
- 动态渲染DOM:每次只更新可见区域的数据。
- 伪造滚动条:通过设置容器高度,保证滚动条看起来像是在处理十万条数据。
让我们一步步拆解代码。
第一步:初始化虚拟列表
class VirtualList {
constructor({ el, data, itemHeight }) {
this.el = el; // 容器元素
this.data = data; // 数据源
this.itemHeight = itemHeight; // 每项固定高度
this.initGhostScrollBar();
this.calculateVisibleRange();
this.render();
this.el.addEventListener('scroll', this.handleScroll.bind(this));
}
// 创建滚动条的“幽灵元素”
initGhostScrollBar() {
this.ghost = document.createElement('div');
this.ghost.style.height = `${this.data.length * this.itemHeight}px`;
this.el.appendChild(this.ghost);
}
解释:
el
:目标容器,滚动区域。data
:你那十万条数据。itemHeight
:假设每个数据项高度固定。ghost
:虚拟的“幽灵元素”,撑开滚动条的高度。
第二步:计算可视范围
// 计算当前可视范围
calculateVisibleRange() {
const scrollTop = this.el.scrollTop;
const visibleCount = Math.ceil(this.el.clientHeight / this.itemHeight);
this.startIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 5); // 上下缓冲5条
this.endIndex = Math.min(this.data.length - 1, this.startIndex + visibleCount + 10); // 下缓冲10条
}
解释:
scrollTop
:滚动条距离顶部的距离。visibleCount
:计算屏幕可见的条目数。- 缓冲渲染:上下各多渲染5到10条数据,减少滚动抖动感。
第三步:渲染可视区域
// 渲染当前可视区域的DOM
render() {
this.el.innerHTML = '';
this.el.appendChild(this.ghost);
const fragment = document.createDocumentFragment();
for (let i = this.startIndex; i <= this.endIndex; i++) {
const item = document.createElement('div');
item.className = 'virtual-item';
item.style.transform = `translateY(${i * this.itemHeight}px)`;
item.innerHTML = this.data[i].content;
fragment.appendChild(item);
}
this.el.appendChild(fragment);
}
解释:
- 清空现有DOM:避免重复渲染。
- 动态设置
transform
:将条目按计算好的高度摆放到正确位置。 - DocumentFragment:优化DOM操作,减少重绘重排。
第四步:滚动事件监听
// 滚动触发重绘
handleScroll() {
requestAnimationFrame(() => {
this.calculateVisibleRange();
this.render();
});
}
}
解释:
- requestAnimationFrame:保证渲染节奏和屏幕刷新率一致,避免卡顿。
- 滚动时重新计算可见范围,并触发重新渲染。
第三幕:动态高度的终极挑战
现实中,列表项往往不是固定高度,比如有图片、折叠面板等。
我们可以用ResizeObserver监听每个元素的动态高度,并缓存下来:
class ProVirtualList extends VirtualList {
constructor(options) {
super(options);
this.heightCache = new Map();
this.observer = new ResizeObserver(entries => {
entries.forEach(entry => {
const index = entry.target.dataset.index;
this.heightCache.set(index, entry.contentRect.height);
});
this.updateGhostHeight();
});
}
结语:十万条数据?再多也不怕!
虚拟列表是前端性能优化的重要工具,通过合理计算可视区域并动态渲染,我们可以让页面滚动丝滑如丝。
产品经理再问:“十万条数据不卡顿吗?”
你就可以微微一笑:“区区十万条,根本不是事。” 😉