JavaScript - DOM 與瀏覽器 API
主題:IntersectionObserver & MutationObserver
簡介
在前端開發中,我們常需要對 DOM 的變化或是元素的可視狀態做出即時回應。傳統上,為了偵測元素是否進入畫面或監控 DOM 的增減,我們往往會使用 scroll、resize 事件或是 setInterval 來輪詢,這樣不僅 效能低下,還容易造成卡頓與記憶體泄漏。
IntersectionObserver 與 MutationObserver 正是為了解決這類需求而誕生的 原生瀏覽器 API。前者讓我們能夠在元素與視口(或其他根元素)交叉時得到通知,適合實作懶載入、無限滾動或動畫觸發;後者則能在 DOM 結構、屬性或文字內容變化時即時捕捉,常用於自動化 UI 更新、偵測動態載入的內容或實作自訂的資料繫結。
本文將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握這兩個觀察者(Observer),讓你的網頁在 效能 與 可維護性 上都有顯著提升。
核心概念
1. IntersectionObserver
IntersectionObserver 觀察目標元素與「根」元素(預設為瀏覽器視口)交叉的比例與時機。它的核心概念包括:
| 參數 | 說明 |
|---|---|
| root | 觀察的根元素,若為 null 則使用視口。 |
| rootMargin | 根元素的外延或內縮,可寫成 CSS margin(如 "0px 0px -50px 0px")。 |
| threshold | 交叉比例的閾值,可為單一數字或陣列,例 0(只要一點進入)或 0.5(進入 50%)。 |
觀察器會在 交叉狀態改變 時呼叫回呼函式,回傳 IntersectionObserverEntry 陣列,裡面包含 isIntersecting、intersectionRatio、boundingClientRect 等資訊。
1.1 基本使用流程
- 建立觀察器:
new IntersectionObserver(callback, options) - 註冊目標元素:
observer.observe(targetElement) - 必要時取消觀察:
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 | 監聽子節點的增刪(addedNodes、removedNodes)。 |
| attributes | 監聽屬性變化(attributeName、oldValue)。 |
| characterData | 監聽文字節點變化。 |
| subtree | 是否遞迴監聽整個子樹。 |
| attributeOldValue / characterDataOldValue | 是否保留舊值,供回呼使用。 |
2.2 基本使用流程
- 建立觀察器:
new MutationObserver(callback) - 開始觀察:
observer.observe(targetNode, options) - 停止觀察:
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 中斷開 |
| 過度觀察 | 設置過寬的 rootMargin 或 threshold 會導致大量回呼,影響效能 |
只觀察必要的元素,適度調整 threshold,使用 debounce 或 throttle 包裝回呼 |
| 回呼執行過長 | IntersectionObserver、MutationObserver 的回呼在同一事件迴圈內執行,若耗時會阻塞 UI |
在回呼內只做最小的判斷與狀態更新,繁重的計算交給 requestIdleCallback 或 setTimeout |
| 觀測的根元素不正確 | 使用 root 為非滾動容器時,觀測結果會與預期不符 |
確認根元素確實是可滾動容器,或直接使用 null(視口) |
忽略 subtree |
想監聽深層子節點卻忘記設定 subtree: true,導致變化被遺漏 |
依需求決定是否開啟 subtree,避免不必要的全樹觀測 |
| 不處理多次觸發 | 例如 IntersectionObserver 在滾動快速時會連續觸發,同一元素多次加入 class |
在回呼內檢查當前狀態(classList.contains)或使用 once 旗標避免重複執行 |
最佳實踐
- 一次建立多個觀察目標:同一個
IntersectionObserver可以觀察多個元素,減少建立實例的成本。 - 使用
WeakMap/WeakSet追蹤已觀察的元素,避免重複observe。 - 對於大量動態元素(如聊天室訊息),建議使用 虛擬化(virtualization)配合觀察器,以降低 DOM 數量。
- 在 React / Vue 等框架 中,將觀察器的建立與斷開放在生命週期 Hook(
useEffect、onMounted)中,確保與組件同步。 - 測試跨瀏覽器相容性:
IntersectionObserver在 IE 不支援,需加上 polyfill;MutationObserver在舊版 Android WebView 可能有 bug。
實際應用場景
| 場景 | 使用哪個觀察者 | 為何適合 |
|---|---|---|
| 圖片、影片懶載入 | IntersectionObserver |
只在即將出現在螢幕時才下載,節省流量與渲染成本。 |
| 無限滾動 / 分頁 | IntersectionObserver + sentinel 元素 |
自動偵測滾動到底部,觸發 API 載入更多資料。 |
| Scroll‑triggered 動畫 | IntersectionObserver |
只在元素可見時加入動畫 class,避免不必要的 CSS 計算。 |
| 動態插入的第三方元件初始化 | MutationObserver |
當外部程式碼在任意時刻插入 DOM 時,自動呼叫初始化邏輯。 |
表單欄位屬性變化(如 disabled、required) |
MutationObserver |
即時同步 UI 提示或驗證規則,提升使用者體驗。 |
| 即時預覽(Markdown、富文字編輯器) | MutationObserver |
捕捉文字節點改變,立即更新預覽區。 |
| 監控廣告區塊是否被封鎖 | MutationObserver |
偵測廣告容器是否被移除或隱藏,進行備援顯示。 |
| 單頁應用路由切換時清理 | 兩者皆可 | 在離開頁面時斷開觀察,避免記憶體泄漏。 |
總結
IntersectionObserver 與 MutationObserver 是 現代瀏覽器 提供的兩大高效觀察工具,分別針對 可視交叉 與 DOM 變化 兩個核心需求。透過正確的建立、設定與斷開,我們可以:
- 降低不必要的事件監聽(如
scroll、resize) - 提升頁面效能,尤其在大量動態內容的網站上
- 簡化程式碼結構,讓 UI 與資料的同步更具可讀性與可維護性
在實務開發中,建議先從 一次觀察多個目標、適度設定 threshold / rootMargin 開始,逐步根據效能檢測(Chrome DevTools 的 Performance 面板)調整。最後別忘了在組件銷毀或頁面離開時 斷開觀察,確保資源得到妥善釋放。
掌握了這兩個 API,你的前端作品將不再受限於笨重的輪詢與繁瑣的事件管理,而是以更 輕量、響應式 的方式呈現給使用者。祝開發順利,玩得開心!