1 万行表格只渲染 18 行:虚拟滚动让我放弃了三个功能

「组件解剖」第一篇。不写怎么用,写为什么必须这样设计------尤其是那些「文档里只说了一句、但背后有硬约束」的地方。

一个 1 万行的表格

后台的订单表有 1 万行。直接全部渲染就是 1 万个 <tr>,滚动时浏览器要重排整张表,长列表会卡到没法用。

标准解法是虚拟滚动:只渲染可视窗口里的那十几行,上下用空白块把滚动高度撑起来。

窗口是算出来的,不是猜的

核心只有 20 行代码,在 table-virtual.ts 里:

ts 复制代码
export function computeWindow(scrollTop, viewportHeight, rowHeight, total, overscan = VIRTUAL_OVERSCAN) {
  const firstVisible = Math.max(0, Math.floor(Math.max(0, scrollTop) / rowHeight));
  const visibleCount = Math.max(0, Math.ceil(viewportHeight / rowHeight));
  const end = Math.min(total, firstVisible + visibleCount + overscan);
  const start = Math.min(Math.max(0, firstVisible - overscan), end);
  return {
    start, end,
    topSpacer: start * rowHeight,
    bottomSpacer: (total - end) * rowHeight,
  };
}

按 maxHeight: 384、size: md 算:视口 384 / 行高 48 = 8 行,加上下各 5 行缓冲,实际只渲染 18 行。1 万行和 18 行之间差了两个数量级,滚动依然丝滑。

这个公式成立的前提只有一条:每一行的高度必须相等 。因为第 N 行的位置只能靠 N × rowHeight 推出来,行高一变,spacer 的高度立刻算错,滚动条会跳。

代价一:展开行不能用了

行高固定是虚拟滚动的命门,而展开行恰恰会改变行高------展开后那一行变成两块高度。

所以代码里直接做了互斥判断:

ts 复制代码
if (props.virtual && hasMaxHeight.value && props.expandable) {
  console.warn("[Table] virtual 模式下不支持 expandable(可变行高与窗口化冲突),已忽略 expandable。");
}

不是「效果差一点」,是直接忽略。这比勉强渲染出一个错位的表格要诚实。

代价二:树形也保不住

树形数据要按 children 做深度优先拍平,而且只展开未收起的子树------也就是说行数是动态的,取决于用户点开了哪些节点。

ts 复制代码
const walk = (rows, depth) => {
  for (const row of rows) {
    const children = row[treeChildrenKey.value];
    const hasChildren = Array.isArray(children) && children.length > 0;
    out.push({ row, index: out.length, depth, hasChildren });
    if (hasChildren && !isTreeCollapsed(row)) walk(children, depth + 1);
  }
};

同一个冲突,同样处理:treeEnabled = treeProps != null && !virtualEnabled,虚拟滚动优先。

代价三:没有 maxHeight 就没有虚拟滚动

虚拟滚动需要一个「有边界的滚动容器」才知道视口多高。不传 maxHeight,组件根本不知道窗口多大,代码里会警告并且不生效:

ts 复制代码
if (props.virtual && !hasMaxHeight.value) {
  console.warn("[Table] virtual 需配合 maxHeight 使用,未设置 maxHeight 时虚拟滚动不生效。");
}

传了 maxHeight 之后,表头靠 position: sticky 固定,滚动时不会跟着走:

还有两个不显眼但会咬人的约束

固定列必须写 width。 左右固定列靠 position: sticky + left/right 偏移实现,没有宽度就没有偏移基准,粘不住。这不是偷懒,是 sticky 的物理限制。

合计行不参与虚拟窗口。 它是独立的一行,sticky 在表体底部,永远可见:

三个尺寸,对应的就是三个行高

size 不只是视觉大小,它直接决定虚拟滚动的计算基数:

ts 复制代码
export const TABLE_ROW_HEIGHTS: Record<Size, number> = { sm: 40, md: 48, lg: 56 };

排序、筛选、斑马纹这些「正常功能」

它们不改变行高,所以和虚拟滚动没有冲突,可以随便叠。

一张表看完取舍

你想同时用 实际结果
virtual + expandable expandable 被忽略,控制台警告
virtual + treeProps 树形被忽略,虚拟滚动优先
virtual + 不传 maxHeight 虚拟滚动不生效,控制台警告
fixed 列 + 不写 width 列宽无基准,固定失效

组件不是功能堆叠

这张表里每一行都是「两个都想要,但物理上不成立」。一个组件成熟不成熟,看的不是功能列表有多长,而是它有没有把不能同时成立的事写进约束里,并且在越界时明确警告,而不是静默降级出一个错位的界面。

下一篇拆 Affix:为什么滚动监听绑在 window 上,放进 overflow 容器就失效。


在线示例:ui.ruanno.top/components/... 源码:gitee.com/ruanno-top/...

相关推荐
仨孩子的爹4 小时前
套打总偏两毫米:预印刷单据的校准方法和三个误区
vue.js
徐小夕4 小时前
JitKnow知识库4.8发布:本体+知识图谱检索引擎,让AI回答不再“一本正经地胡说八道”
前端·vue.js·github
tryCbest4 小时前
Vue3——第 11 篇:组合式 API(上)——setup 与响应式 API 深度解析
前端·javascript·vue.js
tryCbest5 小时前
Vue3——第 13 篇:计算属性与侦听器——computed 与 watch 的进化
前端·javascript·vue.js
闲来没事抠鼻屎6 小时前
标签打印 8 个尺寸公式:小票、价签、吊牌、面单通用
vue.js
时间的情敌6 小时前
Vue3 + Cesium 实战:本地上传 GLB 模型、模型上叠加 GLB、点击控制透明度
前端·vue.js·cesium
小蒜学长8 小时前
基于SpringBoot+Vue的云南地区扶贫助农系统的设计与实现(代码+数据库+LW)
java·vue.js·spring boot·后端·乡村振兴
雪芽蓝域zzs9 小时前
WebStorm Uniapp Support + Uniapp Tool + Uniappx Language Service讲解
vue.js·uni-app·webstorm
OpenTiny社区10 小时前
AI 生成的 Vue 代码,终于能直接用了!
前端·vue.js·github