JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

Troland發表於2019-02-16

原文請查閱這裡,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland

本系列持續更新中,Github 地址請查閱這裡

這是 JavaScript 工作原理的第十章。

網路應用在客戶端日益複雜,這是由很多因素的造成的,比如需要更加豐富的介面互動以提供更加複雜的應用功能,實時計算等等。

網路應用的日益複雜導致無法知曉其生命週期中指定時刻準確的互動介面狀態。

如果你正在構建一些框架或者一個庫,這會更加的困難,比如,你無法通過監測 DOM 來響應並執行一些特定的操作。

概述

MutationObserver  是現代瀏覽器提供的用來檢測 DOM 變化的網頁介面。你可以使用這個介面來監聽新增或者刪除節點,屬性更改,或者文字節點的內容更改。

可以乾點啥好呢?

你可以在以下幾種情況信手拈來 MutationObserver 介面。比如:

  • 通知使用者當前所在的頁面所發生的一些變化。
  • 通過使用一些很棒的 JavaScript 框架來根據 DOM 的變化來動態載入 JavaScript 模組。
  • 可能當你在開發一個所見即所得編輯器的時候,使用 MutationObserver 介面來收集任意時間點上的更改,從而輕鬆地實現撤消/重做功能。

JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

這只是幾個 MutationObserver 的使用場景。

如何使用 MutationObserver

在應用中整合 MutationObserver 是相當簡單的。通過往建構函式 MutationObserver 中傳入一個函式作為引數來初始化一個 MutationObserver 例項,該函式會在每次發生 DOM 發生變化的時候呼叫。MutationObserver 的函式的第一個引數即為單個批處理中的 DOM 變化集。每個變化包含了變化的型別和所發生的更改。

var mutationObserver = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});
複製程式碼

建立的例項物件擁有三個方法:

  • observe-開始進行監聽。接收兩個引數-要觀察的 DOM 節點以及一個配置物件。
  • disconnect-停止監聽變化。
  • takeRecords-觸發回撥前返回最新的批量 DOM 變化。

以下為開始監聽的程式碼片段:

// 開始監聽頁面根元素 HTML 變化。
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});
複製程式碼

現在,假設你寫了一個簡單的 div 元素:

<div id="sample-div" class="test"> Simple div </div>
複製程式碼

可以使用 jQuery 來移除 div 的 class 屬性:

$("#sample-div").removeAttr("class");
複製程式碼

當呼叫 mutationObserver.observe(…) 就可以開始監聽 DOM 變化。

當每次發生 DOM 變化的時候,會列印出各個 MutationRecord 日誌資訊:

JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

這一變化是由移除 class 屬性所引起的。

最後,如果想停止監聽 DOM 變化可以使用如下方法:

// MutationObserver 停止監聽 DOM 變化
mutationObserver.disconnect();
複製程式碼

現在,MutationObserver 瀏覽器相容情況很好:

JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

替代方法

然而,之前 MutationObserver 並沒有被廣泛使用。那麼,當沒有 MutationObserver 的時候,開發者是如何解決監聽 DOM 變化的呢?

有幾下幾種可用的方法:

  • 輪詢
  • MutationEvents
  • CSS 動畫

輪詢

最簡單且粗糙的方法即使用輪詢。使用瀏覽器內建的 setInterval 網頁介面你可以建立一個定時任務來定時檢查 DOM 的變化。當然了,這個方法會顯著地減弱網路應用/網站的效能。

其實,這是可以理解為髒檢查,如果有使用過 AngularJS 應該會有看過其髒檢查所導致的效能問題。在我的另一個系列裡面有稍微介紹了下,具體可以檢視這裡

MutationEvents

早在 2000 年,就推出了 MutationEvents API 。雖然挺管用的,但是每個單一的 DOM 變化都會觸發 mutation 事件,結果又會造成效能問題。現在,MutationEvents 介面已經被廢棄,不久的將來,現代瀏覽器全都將停止支援該介面。

以下是 MutationEvents 的瀏覽器相容情況:

JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

CSS 動畫

依靠 CSS 動畫 是一個有點令人感到新奇的替代方案。這聽起來會讓人有些困惑。大體上,實現思路是這樣的,建立一個動畫,一旦在 DOM 中新增一個元素就會觸發該動畫。開始執行 CSS 動畫的時候就會觸發 animationstart 事件:假設為該事件新增事件監聽器,就可以準確知曉 DOM 中新增元素的時機。動畫的執行時間週期必須非常的短以便讓使用者感知不到,即體驗更佳。

首先,需要一個父級元素,在裡面監聽節點新增事件:

<div id=”container-element”></div>
複製程式碼

為了處理節點的新增,需要建立關鍵幀序列動畫,該序動畫在新增節點的時候啟動:

@keyframes nodeInserted { 
 from { opacity: 0.99; }
 to { opacity: 1; } 
}
複製程式碼

建立好關鍵幀之後,在需要監聽的元素上應用動畫。注意到那個短暫的持續時間-在瀏覽器端動畫痕跡會非常平滑(即使用者會感覺不到有動畫發生):

#container-element * {
 animation-duration: 0.001s;
 animation-name: nodeInserted;
}
複製程式碼

這樣會為 container-element 的所有後代節點新增動畫。當動畫結束,觸發 insertion 事件。

我們需要建立一個函式作為事件監聽器。在函式內部,開始必須使用 event.animationName 程式碼進行檢查,確保是我們所監聽的動畫。

var insertionListener = function(event) {
  // 確保是所監聽的動畫
  if (event.animationName === "nodeInserted") {
    console.log("Node has been inserted: " + event.target);
  }
}
複製程式碼

為父元素繫結事件監聽器:

document.addEventListener(“animationstart”, insertionListener, false); // standard + firefox
document.addEventListener(“MSAnimationStart”, insertionListener, false); // IE
document.addEventListener(“webkitAnimationStart”, insertionListener, false); // Chrome + Safari
複製程式碼

這裡採用了事件委託。

CSS 動畫瀏覽器支援情況:

JavaScript 工作原理之十-使用 MutationObserver 監測 DOM 變化

相比以上幾種替代方案 MutationObserver 有幾點優勢。本質上,它會監聽 DOM 可能發生的每個變化並且效能更優,因其會批量 DOM 變化之後才觸發回撥事件。總之,MutationObserver 的相容性很好,並且還有一些墊片,這些墊片底層是基於 MutationEvents 的。

打個廣告 ^.^

今日頭條招人啦!傳送簡歷到 likun.liyuk@bytedance.com ,即可走快速內推通道,長期有效!國際化PGC部門的JD如下:c.xiumi.us/board/v5/2H…,也可內推其他部門!

本系列持續更新中,Github 地址請查閱這裡

相關文章