監聽DOM加載完成及改變MutationObserver應用

什麼是MutationObserver

接口提供了監視對DOM樹所做更改的能力。它被設計爲舊的MutationEvents功能的替代品,該功能是DOM3 Events規範的一部分。

簡單粗暴,就是監聽DOM樹的變動。

那麼,被代替的 MutationEvents是什麼?

MutationEvents

  1. 首先明確: MutationEvents在MDN中也寫到了,是被 DOMEvent承認在API上有缺陷,反對使用

  2. 缺陷的核心在於兩點:跨瀏覽器支持性能問題

  3. MutationEvents的原理:通過綁定事件監聽DOM 乍一看到感覺很正常,那列一下相關監聽的事件:

DOMAttributeNameChanged	
DOMCharacterDataModified	
DOMElementNameChanged	
DOMNodeInserted	
DOMNodeInsertedIntoDocument	
DOMNodeRemoved	
DOMNodeRemovedFromDocument	
DOMSubtreeModified
  1. 甭記,這麼多事件,各內核各版本瀏覽器想兼容怕是要天荒地老。

  2. 具體說說性能問題(劃重點):

  • (1)事件多,可見的,監聽多項就綁定多項。

  • (2)只要是綁定事件,離不開冒泡捕獲兩種,而監聽的意義,有可能是大量的、頻繁的改動所作出的反應。況且,萬一要監聽多項呢?萬一多層嵌套每層都要監聽呢?萬一父子兄弟祖宗全家桶呢?(寫出來了維護一下試試?)

  • (3)綁定事件立即執行有可能中斷DOM的餘下變動,也就是沒動完事件就觸發了。

原作者吐槽郵件

  1. 接上,這些問題其實使用 觀察者模式就可以不錯的搞定,所以,看 MutationObserver名字就知道了。

一句話說明觀察者模式

因爲本篇主要介紹 MutationObserver,作爲 MutationObserver的設計原理,簡單理解就是

A想看新聞,A就先在B這'交錢(訂閱)',以後有新聞B就給A送報紙,A挑想看的新聞

  • A - 訂閱者 - 通過 MutationObserver得到返回值(得到報紙) - 可以有無數的A來看B

  • B - 被觀察者 - DOM - 變更時發送新的內容(送新報紙) - B決定A

當然,更詳細的觀察者模式不久的將來都會有的。

MutationObserver的改進

針對上面 MutationEvents的缺陷,來說一下 MutationObserver的優勢。

  1. 瀏覽器兼容問題:

const mutationObserver = new MutationObserver(callback);

MutationObserver是構造函數,兼容難度低( IE11以上才支持 ),值得說明的一點,移動端兼容性更佳。

  1. 事件多問題:

const mutationObserver = new MutationObserver((mutations, observer) => {	
    console.log(observer); // 觀察者實例	
    console.log(mutations); // 變動數組	
    mutations.forEach(function(mutation) {	
        console.log(mutation);	
    });	
});

callback作爲監聽事件,返回兩個固定參數 mutationsobservermutations - 變動數組 observer - 觀察者實例

具體要執行的函數呢,往下看

  1. 立即觸發/多次觸發問題:

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 - 觀察指定屬性	
});

注:

  1. attributeFilter/attributeOldValue 優先級高於 attributes

  2. characterDataOldValue 優先級高於 characterData

  3. attributes/characterData/childList(或更高級特定項)至少有一項爲 true

  4. 特定項存在, 對應選項可以 忽略或必須爲 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的應用
  1. vue框架在vue2.0之前,對於 MutationObserver的應用在於 nextTick;原理是利用了 MutationObserver異步回調函數在微任務隊列中排列。具體操作呢,創建一個新節點並觀察,隨意的更新一下它的內容就可以了。

  2. 什麼?爲啥是2.0之前,現在用了 MessageChannel,什麼是 MessageChannel?那是下一個話題。

  3. 爲什麼要用 MutationObserver,或者說它和 PromisesetTimeout的區別在哪裏。vue優先級是 PromiseMutationObserversetTimeout。當 Promise不兼容時選擇 MutationObserver,從功能和性能角度來說兩者基本一致,只是實現略有麻煩,要新建一個節點隨便動一下。setTimeout最後爲了兼容備選使用,原因如下。原因:MutationObserverPromise屬於微任務, setTimeout屬於宏任務;在瀏覽器執行機制裏,每當宏任務執行結束都會進行重新渲染,微任務則在當前宏任務中執行,可以最快的得到最新的更新,如果有對應的DOM操作(回想一下上一篇),在宏任務結束時會一併完成。但如果使用 setTimeout宏任務,更新內容需要等待隊列中前面的全部宏任務執行完畢,並且,如果其中更新內容中有DOM操作,瀏覽器會渲染兩次。

  4. 被棄用的原因:一個兼容性BUG。對於 iOSUIWebView,頁面運行一段時間會中斷,目前原生的 MutationObserver並沒有良好的解決辦法,如果將 IOS10Safari和其他運行環境分開,有些多此一舉。(換一個更好的兼容就是了)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章