实现一个无限滚动列表
无限滚动(Infinite Scroll)是现代内容类产品的标配交互——微博、抖音、朋友圈无一例外。区别于传统的分页(Pagination),无限滚动让用户无需主动翻页,数据会随着滚动悄无声息地追加进来。
但在实现上,无限滚动藏着很多"反直觉"的工程坑:
- 用
scroll事件 + 计算scrollTop来判断是否触底?——高频触发 + 强制同步布局,性能灾难。 - 数据拉够之后不做处理?——DOM 节点疯狂堆积,内存泄漏,最终页面卡死。
本文将系统地探讨正确实现无限滚动的两种主流方案及其核心原理。
方案一:传统 scroll 事件监听(了解缺陷)
最直觉的做法:监听容器的 scroll 事件,判断 scrollTop + clientHeight >= scrollHeight,满足条件时请求下一页数据。
缺陷:
- 即便加了节流,依然在 scroll 回调中读取了
scrollTop、clientHeight、scrollHeight等布局属性,每次都有强制回流的风险。 - 在 DOM 节点越来越多时,计算量持续上涨。
方案二:现代银弹 — IntersectionObserver ⭐
这是目前业界最推荐的方案。
你不需要自己去算 scrollTop,只需要在列表末尾放一个"哨兵元素(Sentinel)",然后用 IntersectionObserver 去监听这个哨兵是否进入了视口。一旦哨兵进入视口,就说明用户滚到了底部,此时加载下一页数据并追加进列表。
浏览器底层使用的是异步的 Intersection 检测,完全不会触发强制回流,性能极高。
完整 React 实现 Demo:
基于 IntersectionObserver 的无限滚动
进阶优化:虚拟列表(DOM 节点回收)
当数据量极大(比如已加载了 10000 条记录),如果仍然把所有 DOM 节点全留在页面上,内存消耗和渲染压力会让页面非常缓慢,这时就需要引入虚拟列表(Virtual List)技术:
- 只渲染视口内可见的几十条数据对应的 DOM 节点("窗口")。
- 当用户滚动时,动态替换"窗口"内渲染的内容,销毁滚出视口的节点,生成新的进入视口的节点。
- 整个列表的 DOM 节点数量始终维持在一个固定的低值(如 30 ~ 50 个)。
这在工程上通常直接使用现成的库,如 react-window、react-virtual、Vue 的 vue-virtual-scroller。
高频面试题剖析
Q:如何实现一个无限滚动功能?当列表数据量非常大时如何优化?
回答思路:
- 方案选择:指出两种方案。传统的
scroll事件监听(需要结合节流)虽然可行,但因为需要读取布局属性有性能风险。推荐使用IntersectionObserver监听列表末尾的"哨兵元素",该 API 是浏览器原生的异步检测机制,完全不触发强制回流,且节省了人工节流的成本。 - 解释核心实现逻辑:哨兵元素放在列表最末尾;
IntersectionObserver监听哨兵进入视口事件 → 触发数据加载 → 新数据追加到列表末尾 → 哨兵元素被重新推到新的底部。 - 展开进阶优化(拿高分):提到当数据量极大(万级以上)时,即便用了懒加载,DOM 节点的持续堆积仍然对内存和渲染性能造成极大压力。此时需要引入虚拟列表(Virtual Scrolling)技术,只渲染可视窗口内的少量节点,滚动时动态替换内容,是大数据列表的终极解法。实际开发中推荐直接使用
react-window或react-virtual等成熟库。

