虚拟滚动与单元格合并本质冲突,需将合并逻辑前置到数据层:生成带merge-start/merge-body标记的扁平化行数组,每行预设高度;渲染时仅merge-start渲染真实DOM,其余为占位;动态高度需用ResizeObserver测量并维护高度与累计位置映射。

单元格合并本身和虚拟滚动在技术逻辑上是冲突的——虚拟滚动依赖“可预测的行高与位置映射”,而合并单元格(尤其是跨行合并)会打破这种线性结构,导致可视区域计算、滚动偏移定位、高度累计等关键环节失效。Vue 3 中没有开箱即用的方案能直接兼容二者,必须做针对性设计。核心思路不是“让虚拟滚动支持合并”,而是“把合并逻辑前置到数据层,再用虚拟滚动渲染已处理好的结构化视图”。
先统一数据结构:把合并逻辑转为“虚拟行”
不要在渲染时动态判断 rowspan/colspan,而是在数据准备阶段就生成带合并标记的扁平化行数组。例如原表格有 1000 行,其中第 5–8 行的“部门”列需合并为一个单元格,那就生成 4 条“虚拟行”:
- 第 0–4 行:普通行(各占 1 行)
- 第 5 行:标记为 merge-start,含完整数据 + rowspan: 4
- 第 6–7 行:标记为 merge-body,只保留空内容或占位符
- 第 8 行:标记为 merge-end(可选),后续恢复普通行
这样整个列表仍是固定长度(比如 1000 行 → 1003 行),每行有明确 height(合并行按总高度预设,如 4×40px=160px),虚拟滚动器就能照常计算 startIndex/endIndex 和 offsetY。
渲染时用 slot 分层控制:合并行单独包裹
在虚拟滚动组件的 <slot></slot> 内,根据当前 item 的标记类型决定渲染方式:
立即学习“前端免费学习笔记(深入)”;
Element Plus Vue3 桌面组件库。在开发 Vue 3.x 页面时调用,使用 Element Plus 组件(如 el-button、el-input、el-form、el-table 等)。
-
merge-start:渲染完整单元格,设置
rowspan,并用position: sticky或transform: translateY()锚定顶部 - merge-body:仅渲染空 div,高度为 0 或透明占位,不绑定事件、不触发响应式开销
- 普通行:按常规单元格渲染
关键点:所有合并相关 DOM 只出现在 merge-start 这一行中,其余行不参与布局计算,避免滚动时因隐藏/显示切换引发重排。
动态高度场景下必须缓存合并行高度
如果合并行内容不固定(如含富文本、图片),首次渲染后需立即测量其真实高度,并更新对应项的 itemHeights Map。建议用 ResizeObserver 监听 merge-start 容器变化,触发后调用 updateAccumulatedHeights() 重新生成累计高度数组,并用二分查找修正后续所有行的偏移位置。此时不能依赖预设 height,必须维护两个映射:
- index → 真实高度(Map 存储)
- index → 累计底部位置(Array 缓存,供 findStartIndex 使用)
避免使用 el-table 的内置 virtual-scroll
Element Plus 的 virtual-scroll 是为规则行设计的,内部硬编码了等高假设,不识别 rowspan,也不提供 merge-aware 的 visibleItems 计算钩子。强行开启会导致:
- 滚动条长度不准(总高度按 1000×40 计算,实际可能是 996×40 + 160)
- 合并行被截断或错位
- 触底加载时 startIndex 计算错误,漏掉部分数据
推荐改用 vue-virtual-scroller 或手写虚拟滚动器,自己控制 size 属性为函数::size="(index) => getItemHeight(items[index])",并在 getItemHeight 中查表返回合并行的真实高度。

















