1. 概述
Mutation Observer API 用來監視 DOM 變動。DOM 的任何變動,比如節點的增減、屬性的變動、文本內容的變動,這個 API 都可以得到通知。
概念上,它很接近事件,可以理解爲 DOM 發生變動就會觸發 Mutation Observer 事件。但是,它與事件有一個本質不同:事件是同步觸發,也就是說,DOM 的變動立刻會觸發相應的事件;Mutation Observer 則是異步觸發,DOM 的變動並不會馬上觸發,而是要等到當前所有 DOM 操作都結束才觸發。
這樣設計是爲了應付 DOM 變動頻繁的特點。舉例來說,如果文檔中連續插入1000個
元素,就會連續觸發1000個插入事件,執行每個事件的回調函數,這很可能造成瀏覽器的卡頓;而 Mutation Observer 完全不同,只在1000個段落都插入結束後纔會觸發,而且只觸發一次。
Mutation Observer 有以下特點:
- 它等待所有腳本任務完成後,纔會運行(即異步觸發方式)。
- 它把 DOM 變動記錄封裝成一個數組進行處理,而不是一條條個別處理 DOM 變動。
- 它既可以觀察 DOM 的所有類型變動,也可以指定只觀察某一類變動。
2. MutationObserver 構造函數
使用時,首先使用 MutationObserver 構造函數,新建一個觀察器實例,同時指定這個實例的回調函數。
var observer = new MutationObserver(callback);
上面代碼中的回調函數,會在每次 DOM 變動後調用。該回調函數接受兩個參數,第一個是變動數組,第二個是觀察器實例,下面是一個例子。
var observer = new MutationObserver(function (mutations, observer) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
3. MutationObserver 的實例方法
3.1 observe()
observe 方法用來啓動監聽,它接受兩個參數。
- 第一個參數:所要觀察的 DOM 節點
- 第二個參數:一個配置對象,指定所要觀察的特定變動
var article = document.querySelector('article');
var options = {
'childList': true,
'attributes':true
} ;
observer.observe(article, options);
上面代碼中,observe方法接受兩個參數,第一個是所要觀察的DOM元素是 article,第二個是所要觀察的變動類型(子節點變動和屬性變動)。
觀察器所能觀察的 DOM 變動類型(即上面代碼的options對象),有以下幾種。
- childList:子節點的變動(指新增,刪除或者更改)。
- attributes:屬性的變動。
- characterData:節點內容或節點文本的變動。
想要觀察哪一種變動類型,就在option對象中指定它的值爲true。需要注意的是,必須同時指定childList、attributes和characterData中的一種或多種,若未均指定將報錯。
除了變動類型,options對象還可以設定以下屬性:
- subtree:布爾值,表示是否將該觀察器應用於該節點的所有後代節點。
- attributeOldValue:布爾值,表示觀察attributes變動時,是否需要記錄變動前的屬性值。
- characterDataOldValue:布爾值,表示觀察characterData變動時,是否需要記錄變動前的值。
- attributeFilter:數組,表示需要觀察的特定屬性(比如[‘class’,’src’])。
// 開始監聽文檔根節點(即<html>標籤)的變動
mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
對一個節點添加觀察器,就像使用addEventListener方法一樣,多次添加同一個觀察器是無效的,回調函數依然只會觸發一次。但是,如果指定不同的options對象,就會被當作兩個不同的觀察器。
下面的例子是觀察新增的子節點。
var insertedNodes = [];
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
for (var i = 0; i < mutation.addedNodes.length; i++)
insertedNodes.push(mutation.addedNodes[i]);
})
});
observer.observe(document, { childList: true });
console.log(insertedNodes);
3.2 disconnect(),takeRecords()
disconnect方法用來停止觀察。調用該方法後,DOM 再發生變動,也不會觸發觀察器。
observer.disconnect();
takeRecords方法用來清除變動記錄,即不再處理未處理的變動。該方法返回變動記錄的數組。
observer.takeRecords();
下面是一個例子。
// 保存所有沒有被觀察器處理的變動
var changes = mutationObserver.takeRecords();
// 停止觀察
mutationObserver.disconnect();
4. MutationRecord 對象
DOM 每次發生變化,就會生成一條變動記錄(MutationRecord 實例)。該實例包含了與變動相關的所有信息。Mutation Observer 處理的就是一個個MutationRecord實例所組成的數組。
MutationRecord對象包含了DOM的相關信息,有如下屬性:
- type:觀察的變動類型(attribute、characterData或者childList)。
- target:發生變動的DOM節點。
- addedNodes:新增的DOM節點。
- removedNodes:刪除的DOM節點。
- previousSibling:前一個同級節點,如果沒有則返回 null。
- nextSibling:下一個同級節點,如果沒有則返回 null。
- attributeName:發生變動的屬性。如果設置了attributeFilter,則只返回預先指定的屬性。
- oldValue:變動前的值。這個屬性只對attribute和characterData變動有效,如果發生childList變動,則返回 null。
5. 應用示例
5.1 子元素的變動
下面的例子說明如何讀取變動記錄。
var callback = function (records){
records.map(function(record){
console.log('Mutation type: ' + record.type);
console.log('Mutation target: ' + record.target);
});
};
var mo = new MutationObserver(callback);
var option = {
'childList': true,
'subtree': true
};
mo.observe(document.body, option);
上面代碼的觀察器,觀察的所有下級節點(childList表示觀察子節點,subtree表示觀察後代節點)的變動。回調函數會在控制檯顯示所有變動的類型和目標節點。
5.2 屬性的變動
下面的例子說明如何追蹤屬性的變動。
var callback = function (records) {
records.map(function (record) {
console.log('Previous attribute value: ' + record.oldValue);
});
};
var mo = new MutationObserver(callback);
var element = document.getElementById('#my_element');
var options = {
'attributes': true,
'attributeOldValue': true
}
mo.observe(element, options);
上面代碼先設定追蹤屬性變動(‘attributes’: true),然後設定記錄變動前的值。實際發生變動時,會將變動前的值顯示在控制檯。
5.3 取代 DOMContentLoaded 事件
網頁加載的時候,DOM 節點的生成會產生變動記錄,因此只要觀察 DOM 的變動,就能在第一時間觸發相關事件,因此也就沒有必要使用DOMContentLoaded事件。
var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
上面代碼中,監聽document.documentElement(即HTML節點)的子節點的變動,subtree屬性指定監聽還包括後代節點。因此,任意一個網頁元素一旦生成,就能立刻被監聽到。
下面的代碼,使用MutationObserver對象封裝一個監聽 DOM 生成的函數。
(function(win){
'use strict';
var listeners = [];
var doc = win.document;
var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
var observer;
function ready(selector, fn){
// 儲存選擇器和回調函數
listeners.push({
selector: selector,
fn: fn
});
if(!observer){
// 監聽document變化
observer = new MutationObserver(check);
observer.observe(doc.documentElement, {
childList: true,
subtree: true
});
}
// 檢查該節點是否已經在DOM中
check();
}
function check(){
// 檢查是否匹配已儲存的節點
for(var i = 0; i < listeners.length; i++){
var listener = listeners[i];
// 檢查指定節點是否有匹配
var elements = doc.querySelectorAll(listener.selector);
for(var j = 0; j < elements.length; j++){
var element = elements[j];
// 確保回調函數只會對該元素調用一次
if(!element.ready){
element.ready = true;
// 對該節點調用回調函數
listener.fn.call(element, element);
}
}
}
}
// 對外暴露ready
win.ready = ready;
})(this);
ready('.foo', function(element){
// ...
});
對比另一個dom監聽方法propretychange,參見另兩篇博客:
1. input輸入框內容變化實時監聽
2. 函數節流
思考: Mutation Observer API 是否需要做函數節流處理?
6. 參考鏈接
[1] RuanYiFeng, Javascript 標準參考教程(alpha)