🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

哨兵模式-无限滾動

前端哨兵模式(Sentinel Pattern)—— 優雅實現滾動加載

一、什麼是哨兵模式?

想像你在排隊買奶茶,你不知道什麼時候輪到你。但如果在你前面第 3 個人身上貼了一張紙條,寫著"看到我就準備點單"——這個人就是"哨兵"

在前端開發中,哨兵模式就是在頁面的某個位置放一個不可見的元素(哨兵),當用戶滾動頁面讓這個元素進入視口時,自動觸發特定操作(比如加載下一頁數據)。

它的核心技術是瀏覽器原生 API —— IntersectionObserver


二、原理

IntersectionObserver 是什麼?

IntersectionObserver(交叉觀察器)是瀏覽器提供的一個 API,用來異步地觀察一個元素與視口(或某個祖先元素)的交叉狀態

簡單說:它能告訴你——"某個元素是否出現在了螢幕上"。

工作流程

┌─────────────────────────────────────┐
│            可視區域(視口)            │
│                                     │
│   ┌─────────────────────────────┐   │
│   │        已加載的列表項         │   │
│   │        ...                  │   │
│   │        列表項 N              │   │
│   └─────────────────────────────┘   │
│                                     │
│   ┌─────────────────────────────┐   │
│   │  🚨 哨兵元素(高度 1px)      │ ← 當它進入視口,觸發回調
│   └─────────────────────────────┘   │
│                                     │
└─────────────────────────────────────┘
         ↓ 觸發回調
    fetchNextPage()  → 加載更多數據
         ↓ 新數據渲染
    哨兵被推到新列表底部 → 等待下次進入視口

關鍵:每次新數據渲染後,哨兵自然地被推到列表最底部,形成一個自動循環:滾到底 → 加載 → 哨兵下移 → 再滾到底 → 再加載…


三、規則

使用哨兵模式時,需要遵守以下規則:

規則 說明
1. 哨兵元素必須始終在列表末尾 只有在最後面,用戶滾到底才能觸發
2. 防止重複觸發 加載中時不要重複請求,用 loading 狀態鎖住
3. 有數據才放哨兵 沒有數據或已加載完畢時,不渲染哨兵元素
4. 及時斷開觀察 元件卸載或條件變化時調用 observer.disconnect() 防止記憶體洩漏
5. 依賴項要完整 useEffect 的依賴數組要包含所有會影響是否加載的狀態
6. 哨兵盡量小 高度 1px 即可,不要影響佈局和用戶體驗

四、用法

基礎用法(React + 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 參數說明

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 不支持)

性能對比

scroll 事件:每秒可能觸發 60+ 次 → 需要 throttle/debounce
哨兵模式:  只在交叉狀態變化時觸發 → 天然高性能 🚀

八、注意事項

  1. 瀏覽器兼容性IntersectionObserver 在現代瀏覽器中均支持(Chrome 51+、Safari 12.1+)。如需兼容老瀏覽器,可引入 polyfill:

    npm install intersection-observer
  2. 避免閃爍:如果頁面初始內容不夠長(不足以滾動),哨兵會立即可見並觸發加載,這其實是正確行為——它會連續加載直到內容填滿螢幕或沒有更多數據。

  3. 配合 useCallback:如果 fetchData 函數作為依賴傳入 useEffect,建議用 useCallback 包裹,避免不必要的 observer 重建。


總結

哨兵模式 = 放一個隱形元素在底部 + 用 IntersectionObserver 監聽它是否出現 + 出現就加載數據

三句話,就是全部核心。剩下的只是條件判斷和狀態管理。它是目前前端實現無限滾動最優雅、性能最好的方案。


原文出處:https://juejin.cn/post/7609927980680757254


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝22  
612
🥈
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付