大家好,我是前端架构师 ,关注微信公众号【程序员大卫】,免费领取精品前端资料。
背景
本文将基于 Vue3 + Element Plus ,实现一个完全自定义的虚拟滚动表格方案 ,支持不等高行、缓存高度、缓冲区渲染,并且与 el-table 解耦。
Element Plus 虽然提供了虚拟滚动,但目前还是测试(beta)阶段,所以暂时先没用了,其实当时写这个组件主要是为了给
Element UI使用的。
一、如何使用
在实际业务中,如果你正在使用 Element Plus 的 el-table ,又遇到了大数据量导致滚动卡顿的问题 ,那么这个组件可以在不改 el-table 任何代码 的前提下,为表格提供一套高性能的虚拟滚动能力。
使用方式非常简单:只需要用 VirtualListTable 包一层 el-table,并通过 change 事件接收当前需要渲染的数据即可。
VirtualListTable负责滚动、计算可视区和缓冲区数据,el-table只负责展示当前这一小段数据,二者完全解耦。
html
<VertualListTable :list-data="tableBigData" @change="renderVirtualData">
<TableBigData :data="tableRenderData" />
</VertualListTable>
二、VirtualListTable 核心实现解析
1️⃣ 虚拟滚动的关键变量
ts
const start = ref(0); // 当前起始索引
const cacheHeight = new Map(); // 行高缓存
let positions: Positions = []; // 每一行的位置 & 高度
let scrollTop = 0;
positions 的结构:
ts
{
id: number | string,
height: number,
top: number
}
这是整个虚拟滚动的"地图"。
2️⃣ 可视区数据计算(含 buffer)
ts
const visibleCount = computed(() =>
Math.ceil(props.height / props.estimatedItemSize)
);
const visibleData = computed(() => {
const startIndex = Math.max(start.value - props.bufferCount, 0);
const endIndex = Math.min(
start.value + visibleCount.value + props.bufferCount,
props.listData.length,
);
return props.listData.slice(startIndex, endIndex);
});
为什么要 buffer?
- 防止滚动时白屏
- 提前渲染上下缓冲区域,提升体验
3️⃣ 滚动时如何快速定位起始索引(关键)
这里使用的是 二分查找。
ts
const getStartIndex = (list: Positions, scrollTop: number) => {
let index = null;
let low = 0;
let high = list.length - 1;
while (low <= high) {
const mid = low + ((high - low) >> 1);
const midVal = list[mid]?.top;
if (midVal <= scrollTop) {
index = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
return index ?? 0;
};
时间复杂度从 O(n) 降到 O(log n),在大数据量下非常关键。
4️⃣ 使用 transform 控制真实 DOM 偏移
ts
const setStartOffset = () => {
const index = Math.max(start.value - props.bufferCount, 0);
const offset = positions[index]?.top ?? 0;
contentRef.value!.style.transform =
`translate3d(0, ${offset}px, 0)`;
};
- 不操作
top - 使用
transform,避免触发重排 - GPU 加速,滚动更顺滑
5️⃣ 不等高行的核心:高度缓存 + ResizeObserver
ts
const updateItemsSize = () => {
getNodes().forEach((node) => {
const id = getNodeId(node);
const height = node.getBoundingClientRect().height;
cacheHeight.set(id, height);
});
};
结合 ResizeObserver:
ts
ro = new ResizeObserver(() => {
if (ignoreResize) return;
updateLayout();
});
解决的问题:
- 表格行高度动态变化
- 文本换行、slot 变化
- 不需要强制固定行高
6️⃣ 占位元素撑开滚动条
这是虚拟滚动中最核心的一步:DOM 只渲染几十行,但滚动条看起来像有几千行
html
<div ref="placeholder" class="placeholder"></div>
ts
const updateTotalHeight = () => {
const lastItem = positions.at(-1);
placeholderRef.value!.style.height =
(lastItem.top + lastItem.height) + 'px';
};
三、与 el-table 的无侵入融合
- 不改 el-table 源码
- 只接管滚动容器
- 所有表格功能照常使用(排序、列、样式)
ts
const initElement = () => {
const $wrapper = containerRef.value
?.querySelector('.el-table__body-wrapper');
const $tableBody = $wrapper
?.querySelector('.el-scrollbar');
contentRef.value?.appendChild($tableBody);
$wrapper?.appendChild(scrollBoxRef.value!);
};
四、TableBigData:保持纯展示
vue
<el-table :data="data">
<el-table-column prop="name" label="Name" />
<el-table-column prop="email" label="Email" />
</el-table>
五、总结
1. 这个方案适合什么场景
- 超大数据量表格(1000+)
- 行高不固定
- 老项目 + Element Plus
- 对滚动性能要求高
2. 方案优势
- 支持不等高行
- 与 el-table 解耦
- 二分查找高性能
- buffer 防白屏
- ResizeObserver 自动修正