表格组件:需求分析与虚拟无限滚动原理
管理后台中最常见的业务组件莫过于表格。无论是用户列表、订单管理、数据报表,几乎所有后台页面都离不开表格的身影。这节课的目标是将 Element Plus 提供的表格组件封装成一个更通用、更易用的业务级表格组件,但在动手写代码之前,先要做好需求调研——搞清楚 Element Plus Table 的现状、虚拟滚动的底层原理、以及社区中有哪些成熟的替代方案。
Element Plus Table V1 vs V2:两种设计思路
Element Plus 提供了两个版本的表格组件:el-table(V1)和 el-table-v2(V2 虚拟化表格)。两者的设计目标截然不同,选型时需要根据实际场景权衡。
V1 Table:功能完善,适合常规场景
V1 版本就是大家日常使用的 el-table,功能非常丰富:
| 功能 | 说明 |
|---|---|
| 带斑马纹表格 | stripe 属性,隔行变色 |
| 固定表头/列 | height + fixed 属性组合 |
| 流体高度 | 根据容器自适应高度 |
| 多级表头 | 嵌套 el-table-column |
| 多选/单选 | selection-column + 事件监听 |
| 排序/筛选 | sortable、filters 属性 |
| 自定义列模板 | #default 作用域插槽 |
| 展开行 | type="expand" 列类型 |
| 合并行/列 | span-method 回调函数 |
V1 的问题是:当页面上渲染的 DOM 节点超过 1000 个(比如 10 列 x 100 行),滚动时就会开始出现肉眼可感知的卡顿。如果用户习惯不点击分页按钮,而是不断向下滚动(懒加载场景),DOM 节点会越积越多,最终导致浏览器渲染引擎跟不上。
V2 Table:虚拟化渲染,为大数据量而生
V2 版本即 el-table-v2,官方定位是"Virtualized Table"。它的核心思路是:不渲染所有行,只渲染当前视窗内可见的行,加上上下各一小段缓冲区。随着用户滚动,动态地销毁离开视窗的 DOM、创建进入视窗的 DOM,保证页面上的 DOM 总数恒定。
V2 新增了几个 V1 没有的功能点:
| 功能 | 说明 |
|---|---|
| 粘性布局 | 类似 V1 的固定表头/列,但实现更轻量 |
| 动态高度行 | 行高不再强制统一,可以根据内容自适应 |
| 横跨/纵跨列 | 单元格合并更灵活 |
| 树形数据 | 支持嵌套数据的展开折叠 |
| 可展开附加信息 | 行内嵌套子表格 |
| 自定义空元素渲染器 | 空数据时的展示完全可控 |
| 浮动遮罩层 | 加载态遮罩,可渲染自定义 Loading |
| 手动滚动控制 | 编程式控制滚动位置 |
同时,V2 减少了一些功能。官方明确表示:减少内置功能是为了降低维护复杂度,让开发者根据需求自行组合。这种"内核精简 + 插件化扩展"的思路在组件库设计中越来越常见。
实际对比中的关键差异
在真实项目中,V2 的使用体验并不完美。以下是 Element Plus GitHub Issues 中反复被提及的问题:
1. 移动端兼容性差
在 iOS Safari 中,V2 的滚动表现远不如桌面端。列头部的横向拖动存在明显延迟,操作体验远差于原生滚动。相关 Issue(#20624)至今仍处于 Open 状态。
2. 固定高度要求
V2 必须设置固定高度(如 height={600}),如果使用 height: 100% 或 100vh,在某些布局下会导致高度无限增长。这个问题的根源在于虚拟滚动需要精确计算视窗高度,而百分比高度依赖父容器的确定性尺寸。
3. 滚动条重复
V2 使用自定义滚动条组件替代浏览器原生滚动条,但两者可能同时出现,导致视觉上出现双滚动条。常见的修复方式是强制隐藏自定义滚动条:
.el-virtual-scrollbar {
display: none !important;
}
css
这样只保留原生滚动条,但会失去 V2 自定义滚动条的一些交互能力。
4. 大量列时的横向滚动卡顿
当列数超过 100 列时,V2 的横向滚动性能也会下降。原因是 V2 只在纵向做了虚拟化,横向仍然是全量渲染(Issue #23914)。
虚拟滚动的核心原理
不管包装成什么组件,虚拟滚动的底层原理都是一样的。理解原理比记住 API 更重要——因为当你遇到性能问题时,原理才能帮你定位瓶颈。
从一个简单的视窗模型说起
想象你站在一扇窗户前看外面的一栋长楼。你每次只能看到窗户框住的那几层楼,楼的其他部分被窗框遮住了。虚拟滚动做的事情就是:当你的视线移动时,只渲染当前可见的那几层楼的 DOM,其余的全部销毁。
更具体地说:
┌─────────────────────────┐
│ 未渲染区域(上方缓冲) │
├─────────────────────────┤
│ ┌─────────────────────┐ │
│ │ 可见区域(视窗) │ │ ← 只渲染这部分 DOM
│ │ 用绝对定位填充 │ │
│ └─────────────────────┘ │
├─────────────────────────┤
│ 未渲染区域(下方缓冲) │
└─────────────────────────┘
text
关键机制:
- 外层容器撑满滚动区间:用一个大的容器元素(高度 = 总行数 x 行高)占据完整的可滚动范围,这样浏览器会显示正确的滚动条长度。
- 绝对定位控制偏移:可见区域的 DOM 元素使用
position: absolute+transform: translateY(offset)来精确放置在视窗中。 - 滚动监听动态更新:监听
scroll事件,根据scrollTop计算当前应该显示哪些行,然后更新 DOM。 - 上下缓冲区:在可见区域上下各多渲染几行(通常是 2-5 行),避免快速滚动时出现白屏闪烁。
- DOM 数量恒定:向上滚出视窗的 DOM 被移除,向下进入视窗的 DOM 被创建。页面上的 DOM 总数始终保持在"可见行数 + 缓冲行数"这个量级。
React 生态中的参考实现
React 生态中有两个经典的虚拟滚动库,它们的实现思路值得参考:
react-window(作者 Brian Vaughn)
- 更轻量,包体积约 6KB
- 提供
FixedSizeList、VariableSizeList、FixedSizeGrid、VariableSizeGrid四种组件 - npm 周下载量长期保持在 300 万以上
- 同一作者还开发了更早期的
react-virtualized,功能更全但体积更大
react-window-infinite-loader
- 基于react-window的高阶组件
- 支持无限滚动加载(滚动到底部时触发数据请求)
- 适合分页接口的无限滚动场景
Vue 生态中的虚拟滚动方案
Vue 官方核心团队维护了一个虚拟滚动方案,但需要注意它的限制条件。
@vueuse/integration(vue-virtual-scroll-list / vue-virtual-scroller)
vue-virtual-scroller 是 Vue 社区中使用最广泛的虚拟滚动库之一,支持动态高度和固定高度两种模式。使用时有几个关键注意点:
- 必须设置容器高度:
VirtualScroller元素必须有明确的高度值,否则无法正确计算可见区域。如果无法设置固定高度,可以使用variable模式,但性能会有所下降。 - 所有 items 应为对象:不支持基础类型(字符串、数字等)直接作为列表项,需要包装成对象。
- 等高模式性能最优:当所有行高相同时,虚拟滚动的计算最简单、性能最好。动态高度需要额外的测量和缓存逻辑。
@vueuse/core 中的虚拟化工具
VueUse 提供了 useVirtualList composable,可以直接配合任意 UI 框架使用。它的特点是更底层、更灵活,但需要自己处理 DOM 渲染逻辑。
性能优化的终极方向:Canvas 渲染
虚拟滚动是一种"折中"方案——它仍然在使用 DOM,只是减少了 DOM 数量。真正的终极方案是像飞书多维表格、Google Sheets 那样,用 Canvas 进行渲染。
为什么 Canvas 更快
| 维度 | DOM 渲染 | Canvas 渲染 |
|---|---|---|
| 渲染机制 | 浏览器维护 DOM 树 + 样式计算 + 布局计算 + 绘制 | 直接操作像素,跳过布局和样式计算 |
| 内存占用 | 每个单元格对应一个 DOM 节点 | 所有单元格共享一个 Canvas 画布 |
| 滚动性能 | 即使虚拟化,大量 DOM 的 diff 仍有开销 | 重绘指定区域,帧率稳定在 60fps |
| 事件处理 | 浏览器原生事件系统 | 需要自己实现 hit-testing |
| 可访问性 | 天然支持屏幕阅读器 | 需要额外实现 ARIA 层 |
| 开发复杂度 | 低,组件化开发 | 高,需要自己实现所有交互 |
Canvas 表格的实现要点
飞书多维表格的 Canvas 渲染方案采用了以下策略:
- 双缓存机制:使用离屏 Canvas 进行预绘制,完成后一次性替换到主 Canvas,避免闪烁。
- 分层绘制:将表格分为背景层、内容层、选中层、交互层,每层独立绘制,变化时只重绘受影响的层。
- 智能缓存:文本测量结果、图标渲染结果、数据映射关系都做了缓存,避免重复计算。
- 帧调度:使用
requestAnimationFrame+ 自适应节流控制,保证渲染不阻塞用户交互。
开源社区中有一些值得关注的 Canvas 表格方案,如 VisActor 的 VTable(字节跳动出品),支持百万级数据的秒级渲染,提供了 Excel 级别的公式计算和单元格编辑功能。
表格组件选型决策树
面对"选哪个表格方案"这个问题,可以按以下逻辑决策:
数据量 < 500 行?
├── 是 → 使用 Element Plus Table V1(功能最全,开发效率最高)
└── 否 → 需要移动端适配?
├── 是 → 考虑 vxe-table 或 AG Grid(移动端兼容性更好)
└── 否 → 列数 < 50?
├── 是 → 使用 Element Plus Table V2(虚拟化纵向滚动)
└── 否 → 需要 Excel 级交互?
├── 是 → Canvas 方案(VTable、Handsontable)
└── 否 → vxe-table(功能丰富 + 虚拟滚动)
text
主流 Vue 表格方案对比(2026年)
| 方案 | 虚拟滚动 | 可编辑 | 树形数据 | 移动端 | 开源协议 | 包体积 |
|---|---|---|---|---|---|---|
| Element Plus Table V1 | 不支持 | 插槽实现 | 支持 | 良好 | MIT | 包含在 Element Plus |
| Element Plus Table V2 | 纵向虚拟化 | 插槽实现 | 支持 | 差 | MIT | 包含在 Element Plus |
| vxe-table | 纵向+横向 | 原生支持 | 支持 | 良好 | MIT | ~200KB |
| AG Grid Community | 纵向+横向 | 原生支持 | 支持 | 良好 | MIT | ~1MB |
| VTable (VisActor) | Canvas 渲染 | 支持 | 支持 | 良好 | MIT | ~300KB |
vxe-table 是国内开发者使用最多的 Element Plus 替代方案之一,由个人开发者徐晓(vxeteam)持续维护。它不仅支持纵向和横向的虚拟滚动,还内置了单元格编辑、数据校验、导入导出等企业级功能。如果你发现 Element Plus Table V2 的功能或性能不能满足需求,vxe-table 是最自然的迁移选择。
本节小结
这节课做的是正式编码前的"需求调研"工作,主要包括三个部分:
- 了解现状:Element Plus 提供了 V1(功能完善)和 V2(虚拟化)两个版本的表格,V2 虽然解决了大数据量下的 DOM 渲染问题,但在移动端兼容性、固定高度要求、滚动条重复等方面仍有缺陷。
- 理解原理:虚拟滚动的本质是"只渲染可见区域 + 绝对定位 + 滚动监听动态更新",这个原理在所有框架和组件库中都是相通的。
- 调研方案:从 React 生态的 react-window 到 Vue 生态的 vue-virtual-scroller,再到 Canvas 渲染的终极方案,每一层都是对上一层的性能升级,但也伴随着开发复杂度的增加。
下一节将基于 Element Plus Table V1 开始封装基础表格组件,把 table + pagination + columns 配置封装成一个可复用的业务组件。选型上先从 V1 起步,是因为 V1 的功能覆盖面更广、稳定性更好,遇到性能瓶颈时再根据本节的分析进行升级。
↑