本文 AI 產出,尚未審核

JavaScript - DOM 與瀏覽器 API

主題:IntersectionObserver & MutationObserver


簡介

在前端開發中,我們常需要對 DOM 的變化或是元素的可視狀態做出即時回應。傳統上,為了偵測元素是否進入畫面或監控 DOM 的增減,我們往往會使用 scrollresize 事件或是 setInterval 來輪詢,這樣不僅 效能低下,還容易造成卡頓與記憶體泄漏。

IntersectionObserverMutationObserver 正是為了解決這類需求而誕生的 原生瀏覽器 API。前者讓我們能夠在元素與視口(或其他根元素)交叉時得到通知,適合實作懶載入、無限滾動或動畫觸發;後者則能在 DOM 結構、屬性或文字內容變化時即時捕捉,常用於自動化 UI 更新、偵測動態載入的內容或實作自訂的資料繫結。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握這兩個觀察者(Observer),讓你的網頁在 效能可維護性 上都有顯著提升。


核心概念

1. IntersectionObserver

IntersectionObserver 觀察目標元素與「根」元素(預設為瀏覽器視口)交叉的比例與時機。它的核心概念包括:

參數 說明
root 觀察的根元素,若為 null 則使用視口。
rootMargin 根元素的外延或內縮,可寫成 CSS margin(如 "0px 0px -50px 0px")。
threshold 交叉比例的閾值,可為單一數字或陣列,例 0(只要一點進入)或 0.5(進入 50%)。

觀察器會在 交叉狀態改變 時呼叫回呼函式,回傳 IntersectionObserverEntry 陣列,裡面包含 isIntersectingintersectionRatioboundingClientRect 等資訊。

1.1 基本使用流程

  1. 建立觀察器new IntersectionObserver(callback, options)
  2. 註冊目標元素observer.observe(targetElement)
  3. 必要時取消觀察observer.unobserve(targetElement)observer.disconnect()

程式碼範例 1:懶載入圖片

// 1. 建立觀察器,當圖片進入視口 0% 時觸發
const lazyObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      // 替換 data-src 為 src,觸發圖片下載
      img.src = img.dataset.src;
      // 觀察完成後取消觀察,避免重複觸發
      observer.unobserve(img);
    }
  });
}, {
  root: null,               // 使用視口作為根
  rootMargin: '0px 0px 200px 0px', // 提前 200px 加載
  threshold: 0              // 任意一點進入即觸發
});

// 2. 掃描所有需要懶載入的圖片
document.querySelectorAll('img[data-src]').forEach(img => {
  lazyObserver.observe(img);
});

重點rootMargin 讓圖片在即將出現在螢幕前就開始下載,提升使用者感受。


程式碼範例 2:無限滾動列表

const sentinel = document.querySelector('#sentinel'); // 放在列表底部的偵測點

const loadMoreObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // 觸發資料載入
      fetchMoreItems().then(items => {
        const list = document.querySelector('#list');
        items.forEach(item => {
          const li = document.createElement('li');
          li.textContent = item;
          list.appendChild(li);
        });
        // 觀察點自動留在最底部,無需手動調整
      });
    }
  });
}, {
  root: null,
  rootMargin: '0px',
  threshold: 1.0 // 完全進入視口才觸發
});

loadMoreObserver.observe(sentinel);

說明sentinel(哨兵)元素永遠位於清單最後,當它完全出現在視口時,就會自動載入更多資料。


程式碼範例 3:觸發動畫(Fade‑in)

const fadeObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('fade-in');
    } else {
      entry.target.classList.remove('fade-in');
    }
  });
}, {
  root: null,
  threshold: 0.3 // 只要元素 30% 以上可見就觸發
});

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  fadeObserver.observe(el);
});
/* CSS 動畫 */
.fade-in {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.animate-on-scroll {
  opacity: 0;
  transform: translateY(30px);
}

技巧:使用 CSS 控制動畫,JavaScript 只負責在適當時機加上/移除 class,保持分離關注點。


2. MutationObserver

MutationObserver 用來監聽 DOM 樹的變化,包括子節點的新增/刪除、屬性變更、文字內容變動等。它是取代已廢棄的 Mutation Events(如 DOMNodeInserted)的現代 API,具備 非同步、批次 的特性,能有效降低重排與重繪的成本。

2.1 觀察選項

選項 說明
childList 監聽子節點的增刪(addedNodesremovedNodes)。
attributes 監聽屬性變化(attributeNameoldValue)。
characterData 監聽文字節點變化。
subtree 是否遞迴監聽整個子樹。
attributeOldValue / characterDataOldValue 是否保留舊值,供回呼使用。

2.2 基本使用流程

  1. 建立觀察器new MutationObserver(callback)
  2. 開始觀察observer.observe(targetNode, options)
  3. 停止觀察observer.disconnect()

程式碼範例 4:自動初始化動態載入的組件

// 假設有一個自訂的 <my-widget> 元素,需要在加入 DOM 後執行 init()
const widgetObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === Node.ELEMENT_NODE && node.matches('my-widget')) {
        // 只要偵測到 my-widget 被插入,就呼叫其 init 方法
        if (typeof node.init === 'function') {
          node.init();
        }
      }
    });
  });
});

widgetObserver.observe(document.body, {
  childList: true,   // 監聽直接子節點的變化
  subtree: true      // 也要監聽更深層的變化
});

說明:許多第三方套件在動態載入時需要手動初始化,使用 MutationObserver 可自動完成此工作,減少程式碼重複。


程式碼範例 5:偵測表單欄位屬性變更(例如 disabled

const form = document.querySelector('#myForm');

const attrObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
      const input = mutation.target;
      console.log(`欄位 ${input.name} 的 disabled 屬性變成 ${input.disabled}`);
      // 例如:自動顯示提示訊息
      const hint = input.nextElementSibling;
      if (hint && hint.classList.contains('hint')) {
        hint.textContent = input.disabled ? '此欄位已被禁用' : '';
      }
    }
  });
});

form.querySelectorAll('input, select, textarea').forEach(el => {
  attrObserver.observe(el, { attributes: true, attributeFilter: ['disabled'] });
});

技巧:使用 attributeFilter 限定只觀察特定屬性,可減少不必要的回呼次數。


程式碼範例 6:即時更新 UI 以反映 DOM 文字變化

const target = document.querySelector('#editable');

const textObserver = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'characterData') {
      console.log('文字內容變更為:', mutation.target.data);
      // 例如:同步到另一個顯示區塊
      document.querySelector('#preview').textContent = mutation.target.data;
    }
  });
});

textObserver.observe(target, {
  characterData: true,
  subtree: true // 文字節點可能在子元素內
});

應用:可用於實作 即時預覽(Markdown 編輯器、富文字編輯器)等功能。


常見陷阱與最佳實踐

陷阱 說明 解決方案
觀察器未斷開 長時間保留 observer 會佔用記憶體,尤其在單頁應用(SPA)切換路由時 在不需要時使用 observer.disconnect(),或在 componentWillUnmount / useEffect cleanup 中斷開
過度觀察 設置過寬的 rootMarginthreshold 會導致大量回呼,影響效能 只觀察必要的元素,適度調整 threshold,使用 debouncethrottle 包裝回呼
回呼執行過長 IntersectionObserverMutationObserver 的回呼在同一事件迴圈內執行,若耗時會阻塞 UI 在回呼內只做最小的判斷與狀態更新,繁重的計算交給 requestIdleCallbacksetTimeout
觀測的根元素不正確 使用 root 為非滾動容器時,觀測結果會與預期不符 確認根元素確實是可滾動容器,或直接使用 null(視口)
忽略 subtree 想監聽深層子節點卻忘記設定 subtree: true,導致變化被遺漏 依需求決定是否開啟 subtree,避免不必要的全樹觀測
不處理多次觸發 例如 IntersectionObserver 在滾動快速時會連續觸發,同一元素多次加入 class 在回呼內檢查當前狀態(classList.contains)或使用 once 旗標避免重複執行

最佳實踐

  1. 一次建立多個觀察目標:同一個 IntersectionObserver 可以觀察多個元素,減少建立實例的成本。
  2. 使用 WeakMap / WeakSet 追蹤已觀察的元素,避免重複 observe
  3. 對於大量動態元素(如聊天室訊息),建議使用 虛擬化(virtualization)配合觀察器,以降低 DOM 數量。
  4. 在 React / Vue 等框架 中,將觀察器的建立與斷開放在生命週期 Hook(useEffectonMounted)中,確保與組件同步。
  5. 測試跨瀏覽器相容性IntersectionObserver 在 IE 不支援,需加上 polyfill;MutationObserver 在舊版 Android WebView 可能有 bug。

實際應用場景

場景 使用哪個觀察者 為何適合
圖片、影片懶載入 IntersectionObserver 只在即將出現在螢幕時才下載,節省流量與渲染成本。
無限滾動 / 分頁 IntersectionObserver + sentinel 元素 自動偵測滾動到底部,觸發 API 載入更多資料。
Scroll‑triggered 動畫 IntersectionObserver 只在元素可見時加入動畫 class,避免不必要的 CSS 計算。
動態插入的第三方元件初始化 MutationObserver 當外部程式碼在任意時刻插入 DOM 時,自動呼叫初始化邏輯。
表單欄位屬性變化(如 disabledrequired MutationObserver 即時同步 UI 提示或驗證規則,提升使用者體驗。
即時預覽(Markdown、富文字編輯器) MutationObserver 捕捉文字節點改變,立即更新預覽區。
監控廣告區塊是否被封鎖 MutationObserver 偵測廣告容器是否被移除或隱藏,進行備援顯示。
單頁應用路由切換時清理 兩者皆可 在離開頁面時斷開觀察,避免記憶體泄漏。

總結

IntersectionObserverMutationObserver現代瀏覽器 提供的兩大高效觀察工具,分別針對 可視交叉DOM 變化 兩個核心需求。透過正確的建立、設定與斷開,我們可以:

  • 降低不必要的事件監聽(如 scrollresize
  • 提升頁面效能,尤其在大量動態內容的網站上
  • 簡化程式碼結構,讓 UI 與資料的同步更具可讀性與可維護性

在實務開發中,建議先從 一次觀察多個目標適度設定 threshold / rootMargin 開始,逐步根據效能檢測(Chrome DevTools 的 Performance 面板)調整。最後別忘了在組件銷毀或頁面離開時 斷開觀察,確保資源得到妥善釋放。

掌握了這兩個 API,你的前端作品將不再受限於笨重的輪詢與繁瑣的事件管理,而是以更 輕量響應式 的方式呈現給使用者。祝開發順利,玩得開心!