skip to content
月与羽

无限滚动加载列表的实现

/ 6 min read

方案对比总览

方案核心思想优点缺点适用场景
1. 滚动事件监听 (Scroll Event)监听滚动容器的 scroll 事件,判断是否滚动到底部。实现简单,无需额外依赖。性能开销大(事件触发频繁),需要手动做节流/防抖优化。列表项较少,或对性能要求不高的快速实现场景。
2. Intersection Observer API使用浏览器原生的 IntersectionObserver API 观察列表底部的“哨兵”元素是否进入视口。性能极佳,避免了频繁的事件计算,代码逻辑清晰。需要浏览器支持(现代浏览器均支持),对于非常老的浏览器需要 Polyfill。强烈推荐的通用方案,兼顾了性能与实现复杂度。
3. 虚拟滚动库 (Virtualization)仅渲染当前视口内可见的列表项,当滚动时动态更新渲染的 DOM 节点。终极性能方案,即使有海量数据(十万、百万级),也能保持极高的渲染性能和低内存占用。实现相对复杂,需要引入第三方库(如 react-window),可能会丢失部分 CSS 效果(如 :nth-child)。当列表数据量巨大,渲染大量 DOM 成为性能瓶颈时的不二之选。

方案一:滚动事件监听 (Scroll Event)

这是最基础直接的实现方式。

核心内容

  1. 绑定事件:在一个可滚动的父容器上(通常是 window 或某个 div),通过 useEffect 绑定 scroll 事件监听器。

  2. 滚动判断:在事件处理函数中,通过判断以下三个值的关系来确定是否已滚动到底部:

    • element.scrollHeight: 元素内容的总高度。

    • element.scrollTop: 元素已滚动的垂直距离。

    • element.clientHeight: 元素的可视区域高度。

    • scrollTop + clientHeight >= scrollHeight - offset (offset 是一个自定义的阈值,如 100px) 时,就认为到达了底部。

  3. 加载数据:触发加载更多数据的函数(如调用 API),并将新数据追加到现有列表状态中。

  4. 性能优化:由于 scroll 事件触发非常频繁,必须使用 debounce (防抖) 或 throttle (节流) 来限制事件处理函数的执行频率,否则会引起性能问题。

  5. 组件卸载:在 useEffect 的清理函数中,务必移除事件监听器,以防止内存泄漏。


方案二:Intersection Observer API

这是目前社区普遍推荐的现代解决方案,它将复杂的滚动计算交给了浏览器本身,更加高效。

核心内容

  1. 设置哨兵 (Sentinel):在列表的末尾放置一个空的 <div> 元素,我们称之为“哨兵”。

  2. 创建观察者:使用 useEffectuseRef 来创建一个 IntersectionObserver 实例,并让它“观察”这个哨兵元素。

  3. 触发回调:当哨兵元素进入或离开浏览器的可视区域时,IntersectionObserver 会执行你提供的回调函数。

  4. 加载数据:在回调函数中,检查哨兵元素是否正在进入视口 (isIntersecting 属性为 true)。如果是,就触发加载更多数据的逻辑。

  5. 自动处理:当新数据加载后,哨兵元素会被推向更下方,自动离开视口。当用户再次滚动到它附近时,上述过程会再次触发,形成一个完美的循环。

这个方案几乎没有性能损耗,因为浏览器在底层优化了可视区域的计算,远比我们手动监听 scroll 事件高效。


方案三:使用虚拟滚动库 (e.g., react-window)

当你的列表项非常非常多(例如上万条)时,即使数据是分批加载的,已经渲染到页面上的 DOM 元素数量也可能多到让浏览器卡顿。虚拟滚动就是为了解决这个问题。

核心内容

  1. 只渲染可见部分:虚拟滚动的核心思想是“欺骗”眼睛。它只创建和渲染当前在屏幕可视区域内的那几个列表项对应的 DOM 元素。

  2. 高度占位:它会计算出整个列表的总高度,并用一个空的占位元素撑开滚动条,让滚动条看起来和所有数据都已渲染时一样。

  3. 动态替换:当你滚动时,库会快速计算哪些项应该被“换入”可视区域,哪些应该被“换出”,然后动态地更新那一小部分 DOM 元素的内容和位置。

  4. 集成无限加载:像 react-window 这样的库通常也提供了与无限加载集成的能力。例如,它可以检测用户是否滚动到了列表的末尾(即滚动位置接近总高度),此时你可以触发加载更多数据的 API。