2020 年 9 月 13 日

突變觀察器

MutationObserver 是內建物件,它會觀察 DOM 元素,並在偵測到變更時觸發回呼函式。

我們將先了解語法,然後探討實際的應用案例,看看這項功能在哪方面可能會有用。

語法

MutationObserver 很容易使用。

首先,我們使用回呼函式建立一個觀察器

let observer = new MutationObserver(callback);

然後將它附加到 DOM 節點

observer.observe(node, config);

config 是布林值選項物件,用於「對哪種類型的變更做出反應」

  • childListnode 的直接子項中的變更,
  • subtreenode 的所有後代中的變更,
  • attributesnode 的屬性,
  • attributeFilter – 屬性名稱陣列,用於僅觀察選定的屬性。
  • characterData – 是否觀察 node.data(文字內容),

其他選項

  • attributeOldValue – 如果為 true,則將屬性的舊值和新值都傳遞給回呼函式(請見下方),否則只傳遞新值(需要 attributes 選項),
  • characterDataOldValue – 如果為 true,將 node.data 的舊值和新值傳遞給回呼(見下文),否則只傳遞新值(需要 characterData 選項)。

然後在任何變更後,執行 callback:變更會在第一個參數中傳遞為 MutationRecord 物件清單,而觀察者本身則作為第二個參數。

MutationRecord 物件有以下屬性

  • type – 變異類型,其中之一
    • "attributes":屬性已修改
    • "characterData":資料已修改,用於文字節點
    • "childList":已新增/移除子元素
  • target – 變更發生位置:"attributes" 的元素,或 "characterData" 的文字節點,或 "childList" 變異的元素
  • addedNodes/removedNodes – 已新增/移除的節點
  • previousSibling/nextSibling – 已新增/移除節點的前一個和後一個同層節點
  • attributeName/attributeNamespace – 已變更屬性的名稱/名稱空間(適用於 XML)
  • oldValue – 前一個值,僅適用於屬性或文字變更,如果設定了對應的選項 attributeOldValue/characterDataOldValue

例如,這裡有一個具有 contentEditable 屬性的 <div>。此屬性允許我們聚焦於它並進行編輯。

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(the changes)
});

// observe everything except attributes
observer.observe(elem, {
  childList: true, // observe direct children
  subtree: true, // and lower descendants too
  characterDataOldValue: true // pass old data to callback
});
</script>

如果我們在瀏覽器中執行此程式碼,然後聚焦於指定的 <div> 並變更 <b>edit</b> 內的文字,console.log 將顯示一個變異

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // other properties empty
}];

如果我們進行更複雜的編輯操作,例如移除 <b>edit</b>,變異事件可能會包含多個變異記錄

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // other properties empty
}, {
  type: "characterData"
  target: <text node>
  // ...mutation details depend on how the browser handles such removal
  // it may coalesce two adjacent text nodes "edit " and ", please" into one node
  // or it may leave them separate text nodes
}];

因此,MutationObserver 允許對 DOM 子樹內的任何變更做出反應。

整合用途

什麼時候可以使用這種方法?

想像一下,當你需要新增一個包含有用功能但也會執行一些不需要的動作的第三方腳本,例如顯示廣告 <div class="ads">不需要的廣告</div>

顯然,第三方腳本沒有提供移除它的機制。

使用 MutationObserver,我們可以偵測不需要的元素在我們的 DOM 中出現時並將其移除。

還有其他情況,當第三方腳本將某些東西新增到我們的文件中,我們希望偵測到它何時發生,以便調整我們的頁面、動態調整大小等。

MutationObserver 允許實作此功能。

架構用途

在架構的觀點來看,MutationObserver 也有好用的地方。

假設我們正在製作一個關於程式設計的網站。自然地,文章和其他資料可能會包含原始碼片段。

此類片段在 HTML 標記中看起來像這樣

...
<pre class="language-javascript"><code>
  // here's the code
  let hello = "world";
</code></pre>
...

為了提高可讀性,同時美化它,我們將在我們的網站上使用 JavaScript 語法突顯程式庫,例如 Prism.js。若要取得 Prism 中上述片段的語法突顯,會呼叫 Prism.highlightElem(pre),它會檢查此類 pre 元素的內容,並將特殊標籤和樣式新增到這些元素中以進行彩色語法突顯,類似於您在此頁面上看到的範例。

我們究竟應該在何時執行這個標記方法?嗯,我們可以在 DOMContentLoaded 事件中執行它,或將腳本放在頁面的底部。當我們的 DOM 準備就緒時,我們可以搜尋元素 pre[class*="language"] 並對它們呼叫 Prism.highlightElem

// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

到目前為止,一切都非常簡單,對吧?我們在 HTML 中找到程式碼片段並標記它們。

現在讓我們繼續。假設我們要從伺服器動態擷取資料。我們將在 本教學課程的後續部分研究此方法。目前,我們只在乎從網路伺服器擷取 HTML 文章並依需求顯示它

let article = /* fetch new content from server */
articleElem.innerHTML = article;

新的 article HTML 可能包含程式碼片段。我們需要對它們呼叫 Prism.highlightElem,否則它們不會被標記。

何時何地對動態載入的文章呼叫 Prism.highlightElem

我們可以將該呼叫附加到載入文章的程式碼,如下所示

let article = /* fetch new content from server */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

…但是,想像一下如果我們在程式碼中有許多載入內容的地方,例如文章、測驗、論壇文章等。我們是否需要在每個地方都放置標記呼叫,以便在載入後標記內容中的程式碼?這不太方便。

如果內容是由第三方模組載入的,那又如何?例如,我們有一個由其他人編寫的論壇,它會動態載入內容,而我們想要為其新增語法標記。沒有人喜歡修補第三方腳本。

幸運的是,還有另一個選項。

我們可以使用 MutationObserver 自動偵測何時將程式碼片段插入頁面並標記它們。

因此,我們將在一個地方處理標記功能,讓我們不必整合它。

動態標記示範

以下為運作範例。

如果您執行此程式碼,它會開始觀察下方的元素並標記出任何出現在那裡的程式碼片段

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // examine new nodes, is there anything to highlight?

    for(let node of mutation.addedNodes) {
      // we track only elements, skip other nodes (e.g. text nodes)
      if (!(node instanceof HTMLElement)) continue;

      // check the inserted element for being a code snippet
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // or maybe there's a code snippet somewhere in its subtree?
      for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
        Prism.highlightElement(elem);
      }
    }
  }

});

let demoElem = document.getElementById('highlight-demo');

observer.observe(demoElem, {childList: true, subtree: true});

這裡下方有一個 HTML 元素和 JavaScript,它使用 innerHTML 動態填入它。

請執行先前的程式碼(上方,觀察該元素),然後執行以下程式碼。您將看到 MutationObserver 如何偵測並標記片段。

一個具有 id="highlight-demo" 的示範元素,執行上述程式碼以觀察它。

以下程式碼會填入其 innerHTML,這會導致 MutationObserver 做出反應並標記其內容

let demoElem = document.getElementById('highlight-demo');

// dynamically insert content with code snippets
demoElem.innerHTML = `A code snippet is below:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>Another one:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

現在我們有了 MutationObserver,它可以追蹤在被觀察元素或整個 document 中的所有標記。我們可以新增/移除 HTML 中的程式碼片段,而不必考慮它。

其他方法

停止觀察節點的方法

  • observer.disconnect() – 停止觀察。

當我們停止觀察時,某些變更可能尚未由觀察者處理。在這種情況下,我們使用

  • observer.takeRecords() – 取得未處理的突變記錄清單,這些記錄已發生,但回呼並未處理它們。

這些方法可以一起使用,如下所示

// get a list of unprocessed mutations
// should be called before disconnecting,
// if you care about possibly unhandled recent mutations
let mutationRecords = observer.takeRecords();

// stop tracking changes
observer.disconnect();
...
observer.takeRecords() 返回的記錄會從處理佇列中移除

observer.takeRecords() 返回的記錄不會呼叫回呼。

垃圾回收互動

觀察者在內部使用節點的弱參照。也就是說,如果節點從 DOM 中移除,並且無法存取,則可以對其進行垃圾回收。

僅觀察 DOM 節點並不會阻止垃圾回收。

摘要

MutationObserver 可以對 DOM 中的變更做出反應,包括屬性、文字內容以及新增/移除元素。

我們可以使用它來追蹤程式碼其他部分引入的變更,以及與第三方腳本整合。

MutationObserver 可以追蹤任何變更。設定檔「觀察事項」選項用於最佳化,而不是浪費資源在不需要的回呼呼叫上。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有建議要改進,請提交 GitHub 議題或提交 pull request,而不是留言。
  • 如果您無法理解文章中的某些內容,請說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,對於多行,請將它們包在 <pre> 標籤中,對於超過 10 行,請使用沙盒(plnkrjsbincodepen…)