普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月9日首页

Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】

2026年2月9日 17:01

大家好,我是前端架构师,关注微信公众号【程序员大卫】,免费领取精品前端资料。

背景

本文将基于 Vue3 + Element Plus,实现一个完全自定义的虚拟滚动表格方案,支持不等高行、缓存高度、缓冲区渲染,并且与 el-table 解耦。

Element Plus 虽然提供了虚拟滚动,但目前还是测试(beta)阶段,所以暂时先没用了,其实当时写这个组件主要是为了给 Element UI 使用的。

一、如何使用

在实际业务中,如果你正在使用 Element Plus 的 el-table,又遇到了大数据量导致滚动卡顿的问题,那么这个组件可以在不改 el-table 任何代码的前提下,为表格提供一套高性能的虚拟滚动能力

使用方式非常简单:只需要用 VirtualListTable 包一层 el-table,并通过 change 事件接收当前需要渲染的数据即可。

  • VirtualListTable 负责滚动、计算可视区和缓冲区数据,
  • el-table 只负责展示当前这一小段数据,二者完全解耦。
<VertualListTable :list-data="tableBigData" @change="renderVirtualData">
  <TableBigData :data="tableRenderData" />
</VertualListTable>

二、VirtualListTable 核心实现解析

1️⃣ 虚拟滚动的关键变量

const start = ref(0);                 // 当前起始索引
const cacheHeight = new Map();        // 行高缓存
let positions: Positions = [];        // 每一行的位置 & 高度
let scrollTop = 0;

positions 的结构:

{
  id: number | string,
  height: number,
  top: number
}

这是整个虚拟滚动的“地图”。

2️⃣ 可视区数据计算(含 buffer)

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️⃣ 滚动时如何快速定位起始索引(关键)

这里使用的是 二分查找

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 偏移

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

const updateItemsSize = () => {
  getNodes().forEach((node) => {
    const id = getNodeId(node);
    const height = node.getBoundingClientRect().height;
    cacheHeight.set(id, height);
  });
};

结合 ResizeObserver

ro = new ResizeObserver(() => {
  if (ignoreResize) return;
  updateLayout();
});

解决的问题:

  • 表格行高度动态变化
  • 文本换行、slot 变化
  • 不需要强制固定行高

6️⃣ 占位元素撑开滚动条

这是虚拟滚动中最核心的一步:DOM 只渲染几十行,但滚动条看起来像有几千行

<div ref="placeholder" class="placeholder"></div>
const updateTotalHeight = () => {
  const lastItem = positions.at(-1);
  placeholderRef.value!.style.height =
    (lastItem.top + lastItem.height) + 'px';
};

三、与 el-table 的无侵入融合

  • 不改 el-table 源码
  • 只接管滚动容器
  • 所有表格功能照常使用(排序、列、样式)
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:保持纯展示

<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 自动修正

源码: github.com/zm8/wechat-…

❌
❌