滚动与虚拟化

滚动来自 overflow

任何 View 只要在某个轴上把 overflow-x / overflow-y 声明为 auto、scroll 或 hidden,它就是那个轴上的滚动容器。不需要换成别的元素:

View({
  style: { height: 480, overflowY: "auto" },
  children: rows,
});

手势、滚轮、滚动链与滚动条都由这一条声明推导:命中路径向上找最近的滚动祖先,滚动条由 Core 用它已经持有的滚动状态绘制,所以滚动帧不进入 Shell。hidden 与 CSS 一致——不给 用户滚动条,程序化滚动仍然有效。

滚动不等于虚拟化。 overflow 只让盒子滚动,它不会去猜你要不要把数据窗口化;下面的 virtual 是一份显式契约,绝不从 overflow 或已经物化的子节点推断出来。

为什么虚拟化放在引擎里

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

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

给 View 一份数据窗口

虚拟化是 View 上的一个属性,不是另一种组件——同一个滚动的盒子既可以装普通子节点,也 可以装一百万行:

View({
  style: { width: 480, height: 640, overflowY: "auto" },
  virtual: {
    axis: "y",
    itemCount: 1_000_000,
    estimatedItemSize: 32,
    getItemKey: (index: number) => `order-${index}`,
    renderItem: (index: number) =>
      View({
        style: { height: 32 },
        children: Text({ value: `第 ${index} 行` }),
      }),
  },
});

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

axis 是单轴的:一个窗口负责 x 或 y,不同时负责两个。

VirtualList 组件仍然可用,它是纵向列表的简写,落到同一套 Core 契约上;需要横向、需要 getItemKey,或者想让同一个盒子既滚动普通内容又开窗口时,用 View 上的 virtual。

可调项

virtual 字段 作用
axis 窗口所在的单轴,x 或 y(默认 y)
itemCount 逻辑项总数
estimatedItemSize 初始尺寸估计,测量后由 Core 纠正
getItemKey 稳定的项标识,用于跨窗口复用
renderItem 物化单项,只对预热窗口内的下标调用
baseOverscanViewports 对称预热范围(视口倍数)
velocityHorizonSeconds 速度投影时长,用于方向预测
maximumAheadViewports 单方向预热上限

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

程序化滚动

scrollX / scrollY 是 View 自己的属性,与是否虚拟化无关,值变化时才发出一次 ScrollTo:

View({ style: { height: 480, overflowY: "auto" }, scrollY: 500_000 * 32, children: rows });

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

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

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

滚轮与触控板

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

嵌套与编辑

滚轮滚动最近的滚动祖先——也就是最近一个声明了 overflow 的 View。指针拖动落在可编辑 文本上时,文本选择优先于滚动拖拽;这个优先级由命中路径深度决定,不需要业务干预。

性能口径

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

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

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

最后更新: