哨兵模式-无限滚动

前端哨兵模式(Sentinel Pattern)------ 优雅实现滚动加载

一、什么是哨兵模式?

想象你在排队买奶茶,你不知道什么时候轮到你。但如果在你前面第 3 个人身上贴了一张纸条,写着"看到我就准备点单"------这个人就是"哨兵"

在前端开发中,哨兵模式 就是在页面的某个位置放一个不可见的元素(哨兵),当用户滚动页面让这个元素进入视口时,自动触发特定操作(比如加载下一页数据)。

它的核心技术是浏览器原生 API ------ IntersectionObserver


二、原理

IntersectionObserver 是什么?

IntersectionObserver(交叉观察器)是浏览器提供的一个 API,用来异步地观察一个元素与视口(或某个祖先元素)的交叉状态

简单说:它能告诉你------"某个元素是否出现在了屏幕上"。

工作流程

scss 复制代码
┌─────────────────────────────────────┐
│            可视区域(视口)            │
│                                     │
│   ┌─────────────────────────────┐   │
│   │        已加载的列表项         │   │
│   │        ...                  │   │
│   │        列表项 N              │   │
│   └─────────────────────────────┘   │
│                                     │
│   ┌─────────────────────────────┐   │
│   │  🚨 哨兵元素(高度 1px)      │ ← 当它进入视口,触发回调
│   └─────────────────────────────┘   │
│                                     │
└─────────────────────────────────────┘
         ↓ 触发回调
    fetchNextPage()  → 加载更多数据
         ↓ 新数据渲染
    哨兵被推到新列表底部 → 等待下次进入视口

关键 :每次新数据渲染后,哨兵自然地被推到列表最底部,形成一个自动循环:滚到底 → 加载 → 哨兵下移 → 再滚到底 → 再加载...


三、规则

使用哨兵模式时,需要遵守以下规则:

规则 说明
1. 哨兵元素必须始终在列表末尾 只有在最后面,用户滚到底才能触发
2. 防止重复触发 加载中时不要重复请求,用 loading 状态锁住
3. 有数据才放哨兵 没有数据或已加载完毕时,不渲染哨兵元素
4. 及时断开观察 组件卸载或条件变化时调用 observer.disconnect() 防止内存泄漏
5. 依赖项要完整 useEffect 的依赖数组要包含所有会影响是否加载的状态
6. 哨兵尽量小 高度 1px 即可,不要影响布局和用户体验

四、用法

基础用法(React + TypeScript)

typescript 复制代码
import { useRef, useEffect, useState } from 'react';

function InfiniteList() {
  const [list, setList] = useState<string[]>([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  // 1️⃣ 创建哨兵元素的 ref
  const sentinelRef = useRef<HTMLDivElement | null>(null);

  // 2️⃣ 加载数据的函数
  const fetchData = async (p: number) => {
    if (loading) return;
    setLoading(true);
    try {
      const res = await fetch(`/api/list?page=${p}`);
      const data = await res.json();
      setList((prev) => [...prev, ...data.items]);
      setHasMore(data.items.length === 20);
      setPage(p);
    } finally {
      setLoading(false);
    }
  };

  // 3️⃣ 设置 IntersectionObserver
  useEffect(() => {
    const el = sentinelRef.current;
    if (!el) return;

    const observer = new IntersectionObserver(
      (entries) => {
        // 当哨兵进入视口,且满足加载条件
        if (entries[0].isIntersecting && hasMore && !loading) {
          fetchData(page + 1);
        }
      },
      { threshold: 0.1 } // 哨兵露出 10% 就触发
    );

    observer.observe(el);

    // 4️⃣ 清理:组件卸载或依赖变化时断开观察
    return () => observer.disconnect();
  }, [hasMore, loading, page]);

  return (
    <div>
      {list.map((item, i) => (
        <div key={i} className="list-item">{item}</div>
      ))}

      {/* 加载中提示 */}
      {loading && <div className="loading">加载中...</div>}

      {/* 5️⃣ 哨兵元素:有更多数据时才渲染 */}
      {hasMore && list.length > 0 && (
        <div ref={sentinelRef} style={{ height: 1 }} />
      )}

      {/* 没有更多了 */}
      {!hasMore && <div className="no-more">没有更多了</div>}
    </div>
  );
}

threshold 参数说明

typescript 复制代码
new IntersectionObserver(callback, {
  threshold: 0.1,   // 元素露出 10% 时触发(推荐)
  // threshold: 0,   // 元素刚刚出现就触发
  // threshold: 1.0, // 元素完全可见才触发
  // rootMargin: '0px 0px 200px 0px', // 提前 200px 触发(预加载)
});

💡 小技巧 :设置 rootMargin: '0px 0px 200px 0px' 可以让用户还没滚到底部就提前加载,体验更流畅。


五、适用场景

✅ 适合使用哨兵模式的场景

场景 说明
长列表滚动加载 商品列表、新闻流、聊天记录等
瀑布流加载 图片瀑布流、Pinterest 风格布局
分页数据替代方案 用无限滚动代替传统"上一页/下一页"
图片懒加载 图片进入视口才开始加载 src
曝光埋点 元素出现在屏幕上时上报埋点数据
动画触发 元素滚动到可视区域时播放动画

❌ 不适合的场景

场景 原因
数据量极少(< 1 页) 没有分页需求,多此一举
需要精确跳转到某页 无限滚动无法直接跳到第 N 页
SEO 要求高的页面 动态加载的内容不利于搜索引擎抓取
需要"回到顶部"后保持位置 无限滚动在页面刷新后无法恢复滚动位置

六、举个生活化的例子 🌰

场景:自助火锅的传送带

想象你在吃回转寿司

  1. 传送带 = 你的页面可滚动区域
  2. 寿司盘子 = 一条条数据
  3. 你的座位前方 = 视口(你能看到的区域)
  4. 最后一个盘子后面的"加菜牌" = 🚨 哨兵元素

当传送带转啊转,"加菜牌"经过你面前时,后厨就知道:盘子快被拿完了,赶紧做新的放上来!

  • 后厨正在做(loading = true)→ 不会重复通知
  • 盘子全上完了(hasMore = false)→ 把"加菜牌"撤掉
  • 还没开始吃(list.length === 0)→ "加菜牌"也不需要放

这就是哨兵模式的全部思想!


七、对比传统方案

方案 实现方式 优点 缺点
监听 scroll 事件 addEventListener('scroll', ...) 兼容性好 频繁触发、需要节流、计算滚动位置复杂
"加载更多"按钮 用户手动点击 简单直接 用户体验差,需要主动操作
🚨 哨兵模式 (IntersectionObserver) 观察哨兵元素 性能好、代码简洁、自动触发 极老浏览器不支持(IE 不支持)

性能对比

bash 复制代码
scroll 事件:每秒可能触发 60+ 次 → 需要 throttle/debounce
哨兵模式:  只在交叉状态变化时触发 → 天然高性能 🚀

八、注意事项

  1. 浏览器兼容性IntersectionObserver 在现代浏览器中均支持(Chrome 51+、Safari 12.1+)。如需兼容老浏览器,可引入 polyfill:
bash 复制代码
npm install intersection-observer
  1. 避免闪烁:如果页面初始内容不够长(不足以滚动),哨兵会立即可见并触发加载,这其实是正确行为------它会连续加载直到内容填满屏幕或没有更多数据。

  2. 配合 useCallback :如果 fetchData 函数作为依赖传入 useEffect,建议用 useCallback 包裹,避免不必要的 observer 重建。


总结

哨兵模式 = 放一个隐形元素在底部 + 用 IntersectionObserver 监听它是否出现 + 出现就加载数据

三句话,就是全部核心。剩下的只是条件判断和状态管理。它是目前前端实现无限滚动最优雅、性能最好的方案。

相关推荐
发现一只大呆瓜1 天前
SSO单点登录:从同域到跨域实战
前端·javascript·面试
发现一只大呆瓜1 天前
告别登录中断:前端双 Token无感刷新
前端·javascript·面试
Cg136269159741 天前
JS-对象-Dom案例
开发语言·前端·javascript
无限大61 天前
《AI观,观AI》:善用AI赋能|让AI成为你深耕核心、推进重心的“最强助手”
前端·后端
烛阴1 天前
Claude Code Skill 从入门到自定义完整教程(Windows 版)
前端·ai编程·claude
lxh01131 天前
数据流的中位数
开发语言·前端·javascript
神仙别闹1 天前
基于NodeJS+Vue+MySQL实现一个在线编程笔试平台
前端·vue.js·mysql
zadyd1 天前
Workflow or ReAct ?
前端·react.js·前端框架
北寻北爱1 天前
vue2和vue3使用less和scss
前端·less·scss
IT_陈寒1 天前
Redis性能提升3倍的5个冷门技巧,90%开发者都不知道!
前端·人工智能·后端