Skip to content

虚拟滚动

为什么在引擎里做

DOM 虚拟列表的长尾问题来自:滚动事件要回到主线程 → 触发 setState → diff → 重排。 主线程一忙,帧就掉。

doper 把窗口计算放进 Core:滚动稳态完全不调用 Shell。Shell 只负责按 Core 规划的 预热窗口物化可见区间;数据没准备好时先画占位,后续帧补建。

用法

ts
createElement("virtualList", {
  width: 480,
  height: 640,
  itemCount: 1_000_000,
  estimatedItemHeight: 32,
  renderItem: (index: number) =>
    createElement("container", {
      width: 480,
      height: 32,
      children: createElement("text", { value: `第 ${index} 行` }),
    }),
});

estimatedItemHeight 只是初始估计。真实高度测量出来后,Core 通过前缀和树(Fenwick) 纠正锚点位置,滚动条不会跳。

可调项

prop作用
baseOverscanViewports对称预热范围(视口倍数)
velocityHorizonSeconds速度投影时长,用于方向预测
maximumAheadViewports单方向预热上限
scrollX / scrollY程序化滚动位置(变化时才发出 ScrollTo)

方向预测会在快速 fling 时优先预热运动方向,而不是对称浪费两侧预算。

程序化滚动

ts
// 通过 prop 变化触发一次 ScrollTo mutation
root.render(createElement("virtualList", { scrollY: 500_000 * 32 /* ... */ }));

或用 root 上的直接操纵 API(用于自定义手势):

ts
root.beginScroll(handle);
root.scrollBy(handle, 0, deltaY, elapsedMs);
root.endScroll(handle); // 交给 Core 估算 fling 速度

handle 来自元素的 ref 回调(NodeHandle)。

滚轮与触控板

滚轮的位移与浏览器原生一致,但传递曲线按输入源分流:高精度 delta(触控板)即时 1:1 应用, 惯性仍由操作系统的事件流提供;离散滚轮格则累加到动画目标并按指数缓出逼近,像浏览器那样硬夹到 内容边界,不产生 overscroll。

嵌套与编辑

指针拖动落在可编辑文本上时,文本选择优先于滚动拖拽;滚轮仍然滚动最近的滚动祖先。 这个优先级由命中路径深度决定,不需要业务干预。

性能口径

固定 fixture(百万行、20,000 帧)的自动 benchmark 是合并门禁的一部分。 当前 P95/P99 为亚微秒级重放,30 分钟连续滚动无不可控内存增长。

真机 P95/P99 与输入延迟属于平台资格采集,不作为工程出口条件——这条界线是刻意的, 避免用无法复现的设备数据阻塞工程进度,也避免用工程数据冒充设备承诺。

Playground 的滚动演示里可以看到实时帧指标。

MIT 许可发布