概述
Mutation Observer API 用來(lái)監(jiān)視 DOM 變動(dòng)阐枣。DOM 的任何變動(dòng),比如節(jié)點(diǎn)的增減篇梭、屬性的變動(dòng)、文本內(nèi)容的變動(dòng)酝枢,這個(gè) API 都可以得到通知很洋。
概念上,它很接近事件隧枫,可以理解為當(dāng) DOM 發(fā)生變動(dòng)喉磁,就會(huì)觸發(fā) Mutation Observer 事件。但是官脓,它與事件有一個(gè)本質(zhì)不同:事件是同步觸發(fā)协怒,也就是說(shuō),DOM 的變動(dòng)立刻會(huì)觸發(fā)相應(yīng)的事件卑笨;Mutation Observer 則是異步觸發(fā)孕暇,DOM 的變動(dòng)并不會(huì)馬上觸發(fā),而是要等到當(dāng)前所有 DOM 操作都結(jié)束才觸發(fā)。
這樣設(shè)計(jì)是為了應(yīng)付 DOM 變動(dòng)頻繁的特點(diǎn)妖滔。舉例來(lái)說(shuō)隧哮,如果在文檔中連續(xù)插入1000個(gè)<p>
元素,就會(huì)連續(xù)觸發(fā)1000個(gè)插入事件座舍,執(zhí)行每個(gè)事件的回調(diào)函數(shù)沮翔,這很可能造成瀏覽器的卡頓;而 Mutation Observer 完全不同曲秉,只在1000個(gè)段落都插入結(jié)束后才會(huì)觸發(fā)采蚀,而且只觸發(fā)一次。
Mutation Observer 有以下特點(diǎn)承二。
- 它等待所有腳本任務(wù)完成后榆鼠,才會(huì)運(yùn)行,即采用異步方式亥鸠。
- 它把 DOM 變動(dòng)記錄封裝成一個(gè)數(shù)組進(jìn)行處理妆够,而不是一條條地個(gè)別處理 DOM 變動(dòng)。
- 它既可以觀察發(fā)生在 DOM 的所有類(lèi)型變動(dòng)负蚊,也可以觀察某一類(lèi)變動(dòng)责静。
MutationObserver 構(gòu)造函數(shù)
使用時(shí),首先使用MutationObserver
構(gòu)造函數(shù)盖桥,新建一個(gè)觀察器實(shí)例灾螃,同時(shí)指定這個(gè)實(shí)例的回調(diào)函數(shù)。
var observer = new MutationObserver(callback);
上面代碼中的回調(diào)函數(shù)揩徊,會(huì)在每次 DOM 變動(dòng)后調(diào)用腰鬼。該回調(diào)函數(shù)接受兩個(gè)參數(shù),第一個(gè)是變動(dòng)數(shù)組塑荒,第二個(gè)是觀察器實(shí)例熄赡,下面是一個(gè)例子。
var observer = new MutationObserver(function (mutations, observer) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
實(shí)例方法
observe()
observe
方法用來(lái)開(kāi)始監(jiān)聽(tīng)齿税,它接受兩個(gè)參數(shù)彼硫。
- 第一個(gè)參數(shù)是所要觀察的 DOM 節(jié)點(diǎn)
- 第二個(gè)參數(shù)是一個(gè)配置對(duì)象,用來(lái)指定所要觀察的特定變動(dòng)
var article = document.querySelector('article');
var options = {
'childList': true,
'attributes':true
} ;
observer.observe(article, options);
上面代碼中凌箕,observe
方法接受兩個(gè)參數(shù)拧篮,第一個(gè)是所要觀察的DOM元素是article
,第二個(gè)是所要觀察的變動(dòng)類(lèi)型(子節(jié)點(diǎn)變動(dòng)和屬性變動(dòng))牵舱。
觀察器所能觀察的 DOM 變動(dòng)類(lèi)型(即上面代碼的options
對(duì)象)串绩,有以下幾種。
- childList:子節(jié)點(diǎn)的變動(dòng)芜壁。
- attributes:屬性的變動(dòng)礁凡。
- characterData:節(jié)點(diǎn)內(nèi)容或節(jié)點(diǎn)文本的變動(dòng)高氮。
- subtree:所有后代節(jié)點(diǎn)的變動(dòng)。
想要觀察哪一種變動(dòng)類(lèi)型顷牌,就在option
對(duì)象中指定它的值為true
剪芍。需要注意的是,如果設(shè)置觀察subtree
的變動(dòng)窟蓝,必須同時(shí)指定childList
罪裹、attributes
和characterData
中的一種或多種。
除了變動(dòng)類(lèi)型疗锐,options
對(duì)象還可以設(shè)定以下屬性:
-
attributeOldValue
:類(lèi)型為布爾值,表示觀察attributes
變動(dòng)時(shí)费彼,是否需要記錄變動(dòng)前的屬性值滑臊。 -
characterDataOldValue
:類(lèi)型為布爾值,表示觀察characterData
變動(dòng)時(shí)箍铲,是否需要記錄變動(dòng)前的值雇卷。 -
attributeFilter
:類(lèi)型為數(shù)組,表示需要觀察的特定屬性(比如['class','src']
)颠猴。
// 開(kāi)始監(jiān)聽(tīng)文檔根節(jié)點(diǎn)(即<html>標(biāo)簽)的變動(dòng)
mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
對(duì)一個(gè)節(jié)點(diǎn)添加觀察器关划,就像使用addEventListener
方法一樣,多次添加同一個(gè)觀察器是無(wú)效的翘瓮,回調(diào)函數(shù)依然只會(huì)觸發(fā)一次贮折。但是,如果指定不同的options
對(duì)象资盅,就會(huì)被當(dāng)作兩個(gè)不同的觀察器调榄。
下面的例子是觀察新增的子節(jié)點(diǎn)。
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);
disconnect()呵扛,takeRecords()
disconnect
方法用來(lái)停止觀察每庆。調(diào)用該方法后,DOM 再發(fā)生變動(dòng)今穿,也不會(huì)觸發(fā)觀察器缤灵。
observer.disconnect();
takeRecords
方法用來(lái)清除變動(dòng)記錄,即不再處理未處理的變動(dòng)蓝晒。該方法返回變動(dòng)記錄的數(shù)組腮出。
observer.takeRecords();
下面是一個(gè)例子。
// 保存所有沒(méi)有被觀察器處理的變動(dòng)
var changes = mutationObserver.takeRecords();
// 停止觀察
mutationObserver.disconnect();
MutationRecord 對(duì)象
DOM 每次發(fā)生變化芝薇,就會(huì)生成一條變動(dòng)記錄利诺。這個(gè)變動(dòng)記錄對(duì)應(yīng)一個(gè)MutationRecord
對(duì)象,該對(duì)象包含了與變動(dòng)相關(guān)的所有信息剩燥。Mutation Observer 處理的是一個(gè)個(gè)MutationRecord
對(duì)象所組成的數(shù)組慢逾。
MutationRecord
對(duì)象包含了DOM的相關(guān)信息立倍,有如下屬性:
-
type
:觀察的變動(dòng)類(lèi)型(attribute
、characterData
或者childList
)侣滩。 -
target
:發(fā)生變動(dòng)的DOM節(jié)點(diǎn)口注。 -
addedNodes
:新增的DOM節(jié)點(diǎn)。 -
removedNodes
:刪除的DOM節(jié)點(diǎn)君珠。 -
previousSibling
:前一個(gè)同級(jí)節(jié)點(diǎn)寝志,如果沒(méi)有則返回null
。 -
nextSibling
:下一個(gè)同級(jí)節(jié)點(diǎn)策添,如果沒(méi)有則返回null
材部。 -
attributeName
:發(fā)生變動(dòng)的屬性。如果設(shè)置了attributeFilter
唯竹,則只返回預(yù)先指定的屬性乐导。 -
oldValue
:變動(dòng)前的值。這個(gè)屬性只對(duì)attribute
和characterData
變動(dòng)有效浸颓,如果發(fā)生childList
變動(dòng)物臂,則返回null
。
應(yīng)用示例
子元素的變動(dòng)
下面的例子說(shuō)明如何讀取變動(dòng)記錄产上。
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);
上面代碼的觀察器棵磷,觀察<body>
的所有下級(jí)節(jié)點(diǎn)(childList
表示觀察子節(jié)點(diǎn),subtree
表示觀察后代節(jié)點(diǎn))的變動(dòng)晋涣∫敲剑回調(diào)函數(shù)會(huì)在控制臺(tái)顯示所有變動(dòng)的類(lèi)型和目標(biāo)節(jié)點(diǎn)。
屬性的變動(dòng)
下面的例子說(shuō)明如何追蹤屬性的變動(dòng)谢鹊。
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);
上面代碼先設(shè)定追蹤屬性變動(dòng)('attributes': true
)规丽,然后設(shè)定記錄變動(dòng)前的值。實(shí)際發(fā)生變動(dòng)時(shí)撇贺,會(huì)將變動(dòng)前的值顯示在控制臺(tái)赌莺。
取代 DOMContentLoaded 事件
網(wǎng)頁(yè)加載的時(shí)候,DOM 節(jié)點(diǎn)的生成會(huì)產(chǎn)生變動(dòng)記錄松嘶,因此只要觀察 DOM 的變動(dòng)艘狭,就能在第一時(shí)間觸發(fā)相關(guān)事件,因此也就沒(méi)有必要使用DOMContentLoaded
事件翠订。
var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
上面代碼中巢音,監(jiān)聽(tīng)document.documentElement
(即HTML節(jié)點(diǎn))的子節(jié)點(diǎn)的變動(dòng),subtree
屬性指定監(jiān)聽(tīng)還包括后代節(jié)點(diǎn)尽超。因此官撼,任意一個(gè)網(wǎng)頁(yè)元素一旦生成,就能立刻被監(jiān)聽(tīng)到似谁。
下面的代碼傲绣,使用MutationObserver
對(duì)象封裝一個(gè)監(jiān)聽(tīng) DOM 生成的函數(shù)掠哥。
(function(win){
'use strict';
var listeners = [];
var doc = win.document;
var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
var observer;
function ready(selector, fn){
// 儲(chǔ)存選擇器和回調(diào)函數(shù)
listeners.push({
selector: selector,
fn: fn
});
if(!observer){
// 監(jiān)聽(tīng)document變化
observer = new MutationObserver(check);
observer.observe(doc.documentElement, {
childList: true,
subtree: true
});
}
// 檢查該節(jié)點(diǎn)是否已經(jīng)在DOM中
check();
}
function check(){
// 檢查是否匹配已儲(chǔ)存的節(jié)點(diǎn)
for(var i = 0; i < listeners.length; i++){
var listener = listeners[i];
// 檢查指定節(jié)點(diǎn)是否有匹配
var elements = doc.querySelectorAll(listener.selector);
for(var j = 0; j < elements.length; j++){
var element = elements[j];
// 確保回調(diào)函數(shù)只會(huì)對(duì)該元素調(diào)用一次
if(!element.ready){
element.ready = true;
// 對(duì)該節(jié)點(diǎn)調(diào)用回調(diào)函數(shù)
listener.fn.call(element, element);
}
}
}
}
// 對(duì)外暴露ready
win.ready = ready;
})(this);
ready('.foo', function(element){
// ...
});
參考鏈接
- Paul Kinlan, Detect DOM changes with Mutation Observers
- Tiffany Brown, Getting to know mutation observers
- Michal Budzynski, JavaScript: The less known parts. DOM Mutations
- Jeff Griffiths, DOM MutationObserver – reacting to DOM changes without killing browser performance
- Addy Osmani, Detect, Undo And Redo DOM Changes With Mutation Observers
- Ryan Morr, Using Mutation Observers to Watch for Element Availability