这套「撑高度 + 绝对定位 + transform 位移」的三层结构,是固定行高虚拟列表的最小可用实现,方向完全正确;真正决定使用体验的,是帖子后面还会涉及的 overscan 缓冲、滚动读写分离和高度上限这三个细节。
overscan 要加。 只渲染视口内那 15 行,快速滚动(尤其是触控板滑动或拖动滚动条)时会出现瞬时空白。做法是渲染 [start - buffer, start + visibleCount + buffer],buffer 取 3~5 行,这样既不会明显增加节点数,又能补上快速滚动时的空白。同时把 start 限制到 [0, total - visibleCount],列表末尾就不会多渲染。
scroll 回调保持只读一次、只写一次。 你已经用 rAF 做节流,接口设计是对的;再提醒两点:一是给监听加上 { passive: true };二是在 rAF 回调里只读一次 scrollTop,之后的 translateY 和行内容更新都基于这个快照,不要在循环里反复读 offsetHeight,否则读和写交替会触发强制同步布局(layout thrashing)。另外,浏览器会把 scrollTop 取整,scrollTop / rowH 可能算出 15.0001 这种值,translateY 要套一层 Math.round,否则文字会发虚或行高出现抖动。
高度上限是固定行高方案的实际限制。 40px × 10000 = 40 万 px 完全安全,但浏览器对元素像素高度有上限(Chrome/Firefox 约 3355 万 px,即 2²⁵ 左右),到百万级会触顶、滚动条比例失真。届时要把「一条长滚动条」改成「按页分段滚动」,或者外层只保留虚拟页偏移。
这里最需要提前考虑的是动态行高。 一旦某行内容变长,startIndex = scrollTop / rowH 这个前提就不成立了。主流做法是:先按预估高度撑起总高度;用 ResizeObserver 实测后回填真实高度;维护位置前缀和数组,用二分查找定位 startIndex。代价是需要做一次位置缓存的重建。
其他容易出问题的地方:列表项里如果有输入框、视频、折叠面板,key 使用不当会把输入框的内容套到其他行;外部有「跳转到某条」「锚点定位」需求时,记得暴露一个 scrollToIndex,并且要先把目标行渲染出来再测量,不能直接调用 scrollIntoView。
正式会员
认证极客






