从零打造 AI 全栈应用(九):无限滚动 InfiniteScroll 的原理与通用组件封装

4 阅读4分钟

本文是「从零打造 AI 全栈应用」系列第 篇。

在上一篇中,我们完成了 图片懒加载与静态资源服务,解决了“图片什么时候加载、从哪里访问”的问题。

这一篇,我们继续解决列表性能的另一半

无限滚动(Infinite Scroll)

如果你正在做:

  • 文章列表 / 信息流
  • AI 内容生成结果列表
  • 评论流 / 动态流

那么 Infinite Scroll 几乎是一个绕不开的基础能力


一、为什么一定要做无限滚动?

很多初学者会觉得:

“后端不是已经分页了吗?前端点「下一页」不就行了?”

这在真实产品中几乎不可接受

1️⃣ 用户体验角度

  • 手动分页会不断打断阅读
  • 移动端操作成本极高

2️⃣ 性能角度

  • 一次性加载全部数据不可行
  • 页面节点数会迅速膨胀

无限滚动 = 分页加载 + 连续体验


二、Infinite Scroll 的本质是什么?

先给结论:

Infinite Scroll 本质上是:自动触发分页请求。

它并没有改变后端模型:

GET /api/posts?page=1&limit=10
GET /api/posts?page=2&limit=10

变化的只是:

  • 触发“下一页”的方式
  • 从“点击按钮” → “滚动到底”

三、核心思想:哨兵节点(Sentinel)

无限滚动的关键,不是滚动本身,而是:

“什么时候该加载下一页?”

现代前端的标准答案是:

哨兵节点 + IntersectionObserver

  • 在列表底部放一个空元素
  • 观察它是否进入视窗
  • 一旦进入,触发 loadMore

这比监听 scroll 更精准,也更高效。


四、为什么要封装成通用组件?

如果你在每个列表页面都写一遍:

  • Observer
  • loading 判断
  • hasMore 判断

那是明显的工程能力不足

一个合格的做法是:

把“滚动逻辑”与“列表内容”彻底解耦。


五、InfiniteScroll 组件的设计目标

在开始写代码前,先明确组件职责:

  • ✅ 负责“什么时候加载”
  • ❌ 不关心“加载什么数据”
  • ❌ 不关心“列表长什么样”

因此它的 API 应该是:

  • children:具体列表内容(高度定制)
  • onLoadMore:加载更多的抽象
  • hasMore:是否还有数据
  • isLoading:防止重复触发

六、InfiniteScroll 通用组件实现

1️⃣ Props 设计

interface InfiniteScrollProps {
  hasMore: boolean;
  isLoading?: boolean;
  onLoadMore: () => void;
  children: React.ReactNode;
}

这是一个非常干净的接口定义


2️⃣ 哨兵节点与 useRef

const sentinelRef = useRef<HTMLDivElement>(null);

原因很简单:

React 不鼓励直接操作 DOM,但允许通过 ref 精确访问。


3️⃣ IntersectionObserver 核心逻辑

useEffect(() => {
  if (!hasMore || isLoading) return;

  const observer = new IntersectionObserver((entries) => {
    if (entries[0].isIntersecting) {
      onLoadMore();
    }
  }, {
    threshold: 0.0,
  });

  if (sentinelRef.current) {
    observer.observe(sentinelRef.current);
  }

  return () => {
    if (sentinelRef.current) {
      observer.unobserve(sentinelRef.current);
    }
  };
}, [onLoadMore, hasMore, isLoading]);

这段代码体现了几个专业点:

  • threshold: 0.0:刚进入视窗就触发
  • hasMore:防止无意义请求
  • isLoading:避免并发触发
  • 清理 observer:防止内存泄漏

这是“能上线”的无限滚动写法。


4️⃣ 组件返回结构

<>
  {children}
  <div ref={sentinelRef} className="h-4" />
  {isLoading && (
    <div className="text-center py-4 text-sm text-muted-foreground">
      加载中...
    </div>
  )}
</>

这里的设计非常关键:

  • 哨兵节点永远在列表底部
  • loading UI 与滚动逻辑解耦

七、InfiniteScroll 如何与后端分页联动?

InfiniteScroll 只负责触发:

onLoadMore();

而真正的数据逻辑应该在业务层:

  • page + 1
  • 调用 /api/posts?page=2&limit=10
  • 拼接列表数据
  • 更新 hasMore / isLoading

职责边界清晰,是组件可复用的前提。


八、Infinite Scroll vs 虚拟列表

这是一个非常常见的面试问题。

场景方案
数据量中等(几十~几百)Infinite Scroll
超大列表(上万)虚拟列表

Infinite Scroll 解决的是“加载策略”,不是“渲染性能”。

下一篇我们会专门讲虚拟列表。


九、面试官视角:你该怎么讲?

如果我问你:

“你们项目的无限滚动是怎么实现的?”

一个成熟的回答应该包括:

  • 后端仍然是分页接口
  • 前端通过哨兵节点触发加载
  • 使用 IntersectionObserver
  • hasMore / isLoading 的控制
  • 封装成通用组件

能讲到这里,说明你具备中高级前端的工程意识


最后

Infinite Scroll 看起来只是一个“小组件”,但它考察的是:

  • API 设计能力
  • 性能意识
  • 抽象能力
  • 与后端协作的边界感

这一篇,已经明显不是“初级前端”的内容了。

我们下一篇见。