什麼是MutationObserver
接口提供了監視對DOM樹所做更改的能力。它被設計爲舊的MutationEvents功能的替代品,該功能是DOM3 Events規範的一部分。
簡單粗暴,就是監聽DOM樹的變動。
那麼,被代替的 MutationEvents
是什麼?
MutationEvents
首先明確:
MutationEvents
在MDN中也寫到了,是被DOMEvent
承認在API上有缺陷,反對使用。缺陷的核心在於兩點:跨瀏覽器支持和性能問題
MutationEvents
的原理:通過綁定事件監聽DOM 乍一看到感覺很正常,那列一下相關監聽的事件:
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
甭記,這麼多事件,各內核各版本瀏覽器想兼容怕是要天荒地老。
具體說說性能問題(劃重點):
(1)事件多,可見的,監聽多項就綁定多項。
(2)只要是綁定事件,離不開冒泡或捕獲兩種,而監聽的意義,有可能是大量的、頻繁的改動所作出的反應。況且,萬一要監聽多項呢?萬一多層嵌套每層都要監聽呢?萬一父子兄弟祖宗全家桶呢?(寫出來了維護一下試試?)
(3)綁定事件立即執行有可能中斷DOM的餘下變動,也就是沒動完事件就觸發了。
原作者吐槽郵件
接上,這些問題其實使用
觀察者模式
就可以不錯的搞定,所以,看MutationObserver
名字就知道了。
一句話說明觀察者模式
因爲本篇主要介紹 MutationObserver
,作爲 MutationObserver
的設計原理,簡單理解就是
A想看新聞,A就先在B這'交錢(訂閱)',以後有新聞B就給A送報紙,A挑想看的新聞
A - 訂閱者 - 通過
MutationObserver
得到返回值(得到報紙) - 可以有無數的A來看BB - 被觀察者 -
DOM
- 變更時發送新的內容(送新報紙) - B決定A
當然,更詳細的觀察者模式不久的將來都會有的。
MutationObserver的改進
針對上面 MutationEvents
的缺陷,來說一下 MutationObserver
的優勢。
瀏覽器兼容問題:
const mutationObserver = new MutationObserver(callback);
MutationObserver
是構造函數,兼容難度低( IE11以上才支持 ),值得說明的一點,移動端兼容性更佳。
事件多問題:
const mutationObserver = new MutationObserver((mutations, observer) => {
console.log(observer); // 觀察者實例
console.log(mutations); // 變動數組
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
callback
作爲監聽事件,返回兩個固定參數 mutations
和 observer
。mutations
- 變動數組 observer
- 觀察者實例
具體要執行的函數呢,往下看
立即觸發/多次觸發問題:
function editContent() {
const content = document.getElementById('content');
console.log(1);
// --------------------------
observer(); // 訂閱
// --------------------------
content.style.background = 'lightblue';
content.style.background = 'red';
console.log(2);
content.innerHTML = 4433;
console.log(3);
const newNode = document.createElement('p');
newNode.innerHTML = 888888;
content.appendChild(newNode);
console.log(4);
}
執行結果:
// 1
// 2
// 3
// 4
// MutationObserver {}
// (4)[MutationRecord, MutationRecord, MutationRecord, MutationRecord]
異步執行,插入微任務隊列,腳本執行後纔會執行。(微任務不能再清楚的帖子)
mutations
參數將監聽的DOM
的所有變更記錄按執行順序
封裝成爲一個數組
返回。可以通過配置項,監聽目標
DOM
下子元素
的變更記錄
MutationObserver的基礎使用
上面已經看到如何通過 MutationObserver
構造函數創建一個實例對象。下一步要綁定 被觀察者
,以及需要觀察哪些變動項。
MutationObserver.observe(dom, options)
啓動監聽,接收兩個參數。
第一參數:被觀察的
DOM
節點第二參數:配置需要觀察的變動項options(記得
MutationEvents
茫茫多的事件嗎,這裏通過配置項完成)
mutationObserver.observe(content, {
attributes: true, // Boolean - 觀察目標屬性的改變
characterData: true, // Boolean - 觀察目標數據的改變(改變前的數據/值)
childList: true, // Boolean - 觀察目標子節點的變化,比如添加或者刪除目標子節點,不包括修改子節點以及子節點後代的變化
subtree: true, // Boolean - 目標以及目標的後代改變都會觀察
attributeOldValue: true, // Boolean - 表示需要記錄改變前的目標屬性值
characterDataOldValue: true, // Boolean - 設置了characterDataOldValue可以省略characterData設置
// attributeFilter: ['src', 'class'] // Array - 觀察指定屬性
});
注:
attributeFilter/attributeOldValue
優先級高於attributes
characterDataOldValue
優先級高於characterData
attributes/characterData/childList
(或更高級特定項)至少有一項爲true
;特定項存在, 對應選項可以
忽略
或必須爲true
附:開發API原文
MutationObserver.disconnect()
停止觀察。調用後不再觸發觀察器,解除訂閱
MutationObserver.takeRecords()
清除變動記錄。即不再處理未處理的變動。該方法返回變動記錄的數組,注意,該方法立即生效。
附:takeRecords變更記錄字段內容 MutationRecord
對象
/*
MutationRecord = {
type:如果是屬性變化,返回"attributes",如果是一個CharacterData節點(Text節點、Comment節點)變化,返回"characterData",節點樹變化返回"childList"
target:返回影響改變的節點
addedNodes:返回添加的節點列表
removedNodes:返回刪除的節點列表
previousSibling:返回分別添加或刪除的節點的上一個兄弟節點,否則返回null
nextSibling:返回分別添加或刪除的節點的下一個兄弟節點,否則返回null
attributeName:返回已更改屬性的本地名稱,否則返回null
attributeNamespace:返回已更改屬性的名稱空間,否則返回null
oldValue:返回值取決於type。對於"attributes",它是更改之前的屬性的值。對於"characterData",它是改變之前節點的數據。對於"childList",它是null
}
*/
MutationObserver的進階應用
監聽JS腳本創建的
DOM渲染完成
監聽圖片/富文本編輯器/節點內容變化及處理
關於
vue
對於MutationObserver
的應用
監聽JS腳本創建的DOM渲染完成
之前有提到, DOM渲染
遇到 腳本阻塞
時會發生類似於"異步"的情況,影響對DOM的後續操作。雖然可以用觸發迴流的方式解決,但是在複雜業務場景中/過量數據場景中並不是十分優秀的選擇。既然 MutationObserver
能夠監聽到 DOM樹
中 子節點
的變化,那麼利用這一點,可以監聽 document
或 父節點
的DOM樹變化。
小巧的栗子:
// html
<div id="content">66666</div>
// js
let time = 4;
let arr = new Array(time);
let content = document.getElementById('content');
let mutationObserver = new MutationObserver(obsCallback); // 創建實例
obs(); // 綁定被觀察者
obstruct(); // 執行阻塞
// 完成創建
function obsCallback(mutations, observer) {
console.log(`創建完成!`);
console.log(observer); // 觀察者實例
console.log(mutations); // 變動數組
}
function obs() {
mutationObserver.observe(content, {
childList: true,
subtree: true,
});
}
function obstruct() {
for (let i = 0; i < arr.length; i++) {
arr[i] = `<div>${i}</div>`;
}
arr.map((item, idx) => {
for(let i = 0; i < 3000; i++) console.log(1)
content.innerHTML += item;
});
}
監聽圖片/富文本編輯器/節點內容變化及處理
之前有一篇講到 contenteditable
屬性,使 DOM
可編輯,做富文本編輯器等應用。對於此類的應用,例如過濾關鍵字或內容,阻止編輯(內容復原),以及無法刪除的圖片水印等一系列操作都可以簡單實現(附1)
阻止編輯的簡陋栗子:
// html
<div id="content">66666</div>
// js
function obsCallback(mutations, observer) {
console.log(observer); // 觀察者實例
console.log(mutations); // 變動數組
mutations.forEach(mutation => {
if (mutation.target.contentEditable === 'true') {
mutation.target.setAttribute('contenteditable', 'false');
}
})
}
function obs() {
mutationObserver.observe(content, {
// attributes: true,
attributeFilter: ['contenteditable']
// characterData: true,
// childList: true,
// subtree: true,
});
}
附:實現水印的不可刪除
關於 vue
對於 MutationObserver
的應用
vue
框架在vue2.0之前,對於MutationObserver
的應用在於nextTick
;原理是利用了MutationObserver
異步回調函數在微任務隊列中排列。具體操作呢,創建一個新節點並觀察,隨意的更新一下它的內容就可以了。什麼?爲啥是2.0之前,現在用了
MessageChannel
,什麼是MessageChannel
?那是下一個話題。爲什麼要用
MutationObserver
,或者說它和Promise
與setTimeout
的區別在哪裏。vue
優先級是Promise
、MutationObserver
、setTimeout
。當Promise
不兼容時選擇MutationObserver
,從功能和性能角度來說兩者基本一致,只是實現略有麻煩,要新建一個節點隨便動一下。setTimeout
最後爲了兼容備選使用,原因如下。原因:MutationObserver
與Promise
屬於微任務,setTimeout
屬於宏任務;在瀏覽器執行機制裏,每當宏任務執行結束都會進行重新渲染,微任務則在當前宏任務中執行,可以最快的得到最新的更新,如果有對應的DOM操作(回想一下上一篇),在宏任務結束時會一併完成。但如果使用setTimeout
宏任務,更新內容需要等待隊列中前面的全部宏任務執行完畢,並且,如果其中更新內容中有DOM操作,瀏覽器會渲染兩次。被棄用的原因:一個兼容性BUG。對於
iOSUIWebView
,頁面運行一段時間會中斷,目前原生的MutationObserver
並沒有良好的解決辦法,如果將IOS10Safari
和其他運行環境分開,有些多此一舉。(換一個更好的兼容就是了)