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

上節講到DOM的操做是"同步"仍是"異步",怎麼準確的監聽DOM到底啥時候渲染成功了呢——MutationObserver。javascript

1、什麼是MutationObserver

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

來源:MDNvue

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

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

2、MutationEvents

  1. 首先明確:MutationEvents在MDN中也寫到了,是被DOM Event認可在API上有缺陷,反對使用
  2. 缺陷的核心在於兩點:跨瀏覽器支持性能問題
  3. MutationEvents的原理:經過綁定事件監聽DOM 乍一看到感受很正常,那列一下相關監聽的事件:
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
複製代碼
  1. 甭記,這麼多事件,各內核各版本瀏覽器想兼容怕是要天荒地老。
  2. 具體說說性能問題(劃重點):
    • (1)事件多,可見的,監聽多項就綁定多項。
    • (2)只要是綁定事件,離不開冒泡捕獲兩種,而監聽的意義,有多是大量的、頻繁的改動所做出的反應。何況,萬一要監聽多項呢?萬一多層嵌套每層都要監聽呢?萬一父子兄弟祖宗全家桶呢?(寫出來了維護一下試試?)
    • (3)綁定事件當即執行有可能中斷DOM的餘下變更,也就是沒動完事件就觸發了。

原做者吐槽郵件github

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

3、一句話說明觀察者模式

由於本篇主要介紹MutationObserver,做爲MutationObserver的設計原理,簡單理解就是web

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

  • A - 訂閱者 - 經過MutationObserver獲得返回值(獲得報紙) - 能夠有無數的A來看B
  • B - 被觀察者 - DOM - 變動時發送新的內容(送新報紙) - B決定A

固然,更詳細的觀察者模式不久的未來都會有的。windows

4、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子元素的變動記錄

5、MutationObserver的基礎使用

上面已經看到如何經過MutationObserver構造函數建立一個實例對象。 下一步要綁定被觀察者,以及須要觀察哪些變更項。

1. 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原文

2. MutationObserver.disconnect()

中止觀察。調用後再也不觸發觀察器,解除訂閱 注:當完成監聽後,儘可量記得解除訂閱

3. 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
}
*/
複製代碼

6、MutationObserver的進階應用

  • 監聽JS腳本建立的DOM渲染完成
  • 監聽圖片/富文本編輯器/節點內容變化及處理
  • 關於vue對於MutationObserver的應用
1. 監聽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;
	});
}

複製代碼
2. 監聽圖片/富文本編輯器/節點內容變化及處理

以前有一篇講到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,
    });
}
複製代碼

附:實現水印的不可刪除

3. 關於vue對於MutationObserver的應用
  • vue框架在vue2.0以前,對於MutationObserver的應用在於nextTick; 原理是利用了MutationObserver異步回調函數在微任務隊列中排列。 具體操做呢,建立一個新節點並觀察,隨意的更新一下它的內容就能夠了。

  • 什麼?爲啥是2.0以前,如今用了MessageChannel,什麼是MessageChannel?那是下一個話題。

  • 爲何要用MutationObserver,或者說它和PromisesetTimeout的區別在哪裏。 vue優先級是PromiseMutationObserversetTimeout。 當Promise不兼容時選擇MutationObserver,從功能和性能角度來講二者基本一致,只是實現略有麻煩,要新建一個節點隨便動一下。 setTimeout最後爲了兼容備選使用,緣由以下。

  • 緣由: MutationObserverPromise屬於微任務,setTimeout屬於宏任務; 在瀏覽器執行機制裏,每當宏任務執行結束都會進行從新渲染,微任務則在當前宏任務中執行,能夠最快的獲得最新的更新,若是有對應的DOM操做(回想一下上一篇),在宏任務結束時會一併完成。 但若是使用setTimeout宏任務,更新內容須要等待隊列中前面的所有宏任務執行完畢,而且,若是其中更新內容中有DOM操做,瀏覽器會渲染兩次。

  • 被棄用的緣由: 一個兼容性BUG。對於iOS UIWebView,頁面運行一段時間會中斷,目前原生的MutationObserver並無良好的解決辦法,若是將IOS10 Safari和其餘運行環境分開,有些畫蛇添足。(換一個更好的兼容就是了) 原回覆


參考引用:

  1. developer.mozilla.org/en-US/docs/…
  2. segmentfault.com/a/119000001…
  3. javascript.ruanyifeng.com/dom/mutatio…
  4. docs.microsoft.com/en-us/previ…
相關文章
相關標籤/搜索