Javascript DOM 變動(dòng)觀察器(Mutation observer)

2023-02-17 10:55 更新

?MutationObserver? 是一個(gè)內(nèi)建對象,它觀察 DOM 元素,并在檢測到更改時(shí)觸發(fā)回調(diào)。

我們將首先看一下語法,然后探究一個(gè)實(shí)際的用例,以了解它在什么地方有用。

語法

MutationObserver 使用簡單。

首先,我們創(chuàng)建一個(gè)帶有回調(diào)函數(shù)的觀察器:

let observer = new MutationObserver(callback);

然后將其附加到一個(gè) DOM 節(jié)點(diǎn):

observer.observe(node, config);

config 是一個(gè)具有布爾選項(xiàng)的對象,該布爾選項(xiàng)表示“將對哪些更改做出反應(yīng)”:

  • ?childList? —— ?node? 的直接子節(jié)點(diǎn)的更改,
  • ?subtree? —— ?node? 的所有后代的更改,
  • ?attributes? —— ?node? 的特性(attribute),
  • ?attributeFilter? —— 特性名稱數(shù)組,只觀察選定的特性。
  • ?characterData? —— 是否觀察 ?node.data?(文本內(nèi)容),

其他幾個(gè)選項(xiàng):

  • ?attributeOldValue? —— 如果為 ?true?,則將特性的舊值和新值都傳遞給回調(diào)(參見下文),否則只傳新值(需要 ?attributes? 選項(xiàng)),
  • ?characterDataOldValue? —— 如果為 ?true?,則將 ?node.data? 的舊值和新值都傳遞給回調(diào)(參見下文),否則只傳新值(需要 ?characterData? 選項(xiàng))。

然后,在發(fā)生任何更改后,將執(zhí)行“回調(diào)”:更改被作為一個(gè) MutationRecord 對象列表傳入第一個(gè)參數(shù),而觀察器自身作為第二個(gè)參數(shù)。

MutationRecord 對象具有以下屬性:

  • ?type? —— 變動(dòng)類型,以下類型之一:
    • ?"attributes"?:特性被修改了,
    • ?"characterData"?:數(shù)據(jù)被修改了,用于文本節(jié)點(diǎn),
    • ?"childList"?:添加/刪除了子元素。
  • ?target? —— 更改發(fā)生在何處:?"attributes"? 所在的元素,或 ?"characterData"? 所在的文本節(jié)點(diǎn),或 ?"childList"? 變動(dòng)所在的元素,
  • ?addedNodes/removedNodes? —— 添加/刪除的節(jié)點(diǎn),
  • ?previousSibling/nextSibling? —— 添加/刪除的節(jié)點(diǎn)的上一個(gè)/下一個(gè)兄弟節(jié)點(diǎn),
  • ?attributeName/attributeNamespace? —— 被更改的特性的名稱/命名空間(用于 XML),
  • ?oldValue? —— 之前的值,僅適用于特性或文本更改,如果設(shè)置了相應(yīng)選項(xiàng) ?attributeOldValue/characterDataOldValue?。

例如,這里有一個(gè) <div>,它具有 contentEditable 特性。該特性使我們可以聚焦和編輯元素。

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

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

// 觀察除了特性之外的所有變動(dòng)
observer.observe(elem, {
  childList: true, // 觀察直接子節(jié)點(diǎn)
  subtree: true, // 及其更低的后代節(jié)點(diǎn)
  characterDataOldValue: true // 將舊的數(shù)據(jù)傳遞給回調(diào)
});
</script>

如果我們在瀏覽器中運(yùn)行上面這段代碼,并聚焦到給定的 <div> 上,然后更改 <b>edit</b> 中的文本,console.log 將顯示一個(gè)變動(dòng):

mutationRecords = [{
  type: "characterData",
  oldValue: "edit",
  target: <text node>,
  // 其他屬性為空
}];

如果我們進(jìn)行更復(fù)雜的編輯操作,例如刪除 <b>edit</b>,那么變動(dòng)事件可能會(huì)包含多個(gè)變動(dòng)記錄:

mutationRecords = [{
  type: "childList",
  target: <div#elem>,
  removedNodes: [<b>],
  nextSibling: <text node>,
  previousSibling: <text node>
  // 其他屬性為空
}, {
  type: "characterData"
  target: <text node>
  // ...變動(dòng)的詳細(xì)信息取決于瀏覽器如何處理此類刪除
  // 它可能是將兩個(gè)相鄰的文本節(jié)點(diǎn) "edit " 和 ", please" 合并成一個(gè)節(jié)點(diǎn),
  // 或者可能將它們留在單獨(dú)的文本節(jié)點(diǎn)中
}];

因此,MutationObserver 允許對 DOM 子樹中的任何更改作出反應(yīng)。

用于集成

在什么時(shí)候可能有用?

想象一下,你需要添加一個(gè)第三方腳本,該腳本不僅包含有用的功能,還會(huì)執(zhí)行一些我們不想要的操作,例如顯示廣告 <div class="ads">Unwanted ads</div>

當(dāng)然,第三方腳本沒有提供刪除它的機(jī)制。

使用 MutationObserver,我們可以監(jiān)測到我們不需要的元素何時(shí)出現(xiàn)在我們的 DOM 中,并將其刪除。

還有一些其他情況,例如第三方腳本會(huì)將某些內(nèi)容添加到我們的文檔中,并且我們希望檢測出這種情況何時(shí)發(fā)生,以調(diào)整頁面,動(dòng)態(tài)調(diào)整某些內(nèi)容的大小等。

MutationObserver 使我們能夠?qū)崿F(xiàn)這種需求。

用于架構(gòu)

從架構(gòu)的角度來看,在某些情況下,MutationObserver 有不錯(cuò)的作用。

假設(shè)我們正在建立一個(gè)有關(guān)編程的網(wǎng)站。自然地,文章和其他材料中可能包含源代碼段。

在 HTML 標(biāo)記(markup)中的此類片段如下所示:

...
<pre class="language-javascript"><code>
  // 這里是代碼
  let hello = "world";
</code></pre>
...

為了提高可讀性,同時(shí)對其進(jìn)行美化,我們將在我們的網(wǎng)站上使用 JavaScript 語法高亮顯示庫,例如 Prism.js。為了使用 Prism 對以上代碼片段進(jìn)行語法高亮顯示,我們調(diào)用了 Prism.highlightElem(pre),它會(huì)檢查此類 pre 元素的內(nèi)容,并為這些元素添加特殊的標(biāo)簽(tag)和樣式,以進(jìn)行彩色語法高亮顯示,類似于你在本文的示例中看到的那樣。

那么,我們應(yīng)該在什么時(shí)候執(zhí)行該高亮顯示方法呢?我們可以在 DOMContentLoaded 事件中執(zhí)行,或者將腳本放在頁面的底部。DOM 就緒后,我們可以搜索元素 pre[class*="language"] 并對其調(diào)用 Prism.highlightElem

// 高亮顯示頁面上的所有代碼段
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

到目前為止,一切都很簡單,對吧?我們找到 HTML 中的代碼片段并高亮顯示它們。

現(xiàn)在讓我們繼續(xù)。假設(shè)我們要從服務(wù)器動(dòng)態(tài)獲取資料。我們將 在本教程的后續(xù)章節(jié) 中學(xué)習(xí)進(jìn)行此操作的方法。目前,只需要關(guān)心我們從網(wǎng)絡(luò)服務(wù)器獲取 HTML 文章并按需顯示:

let article = /* 從服務(wù)器獲取新內(nèi)容 */
articleElem.innerHTML = article;

新的 article HTML 可能包含代碼段。我們需要對其調(diào)用 Prism.highlightElem,否則它們將不會(huì)被高亮顯示。

對于動(dòng)態(tài)加載的文章,應(yīng)該在何處何時(shí)調(diào)用 Prism.highlightElem

我們可以將該調(diào)用附加到加載文章的代碼中,如下所示:

let article = /* 從服務(wù)器獲取新內(nèi)容 */
articleElem.innerHTML = article;

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

……但是,想象一下,如果代碼中有很多地方都是在加載內(nèi)容:文章,測驗(yàn)和論壇帖子等。我們是否需要在每個(gè)地方都附加一個(gè)高亮顯示調(diào)用,以在內(nèi)容加載完成后,高亮內(nèi)容中的代碼。那很不方便。

并且,如果內(nèi)容是由第三方模塊加載的,該怎么辦?例如,我們有一個(gè)由其他人編寫的論壇,該論壇可以動(dòng)態(tài)加載內(nèi)容,并且我們想為其添加語法高亮顯示。沒有人喜歡修補(bǔ)第三方腳本。

幸運(yùn)的是,還有另一種選擇。

我們可以使用 MutationObserver 來自動(dòng)檢測何時(shí)在頁面中插入了代碼段,并高亮顯示它們。

因此,我們在一個(gè)地方處理高亮顯示功能,從而使我們無需集成它。

動(dòng)態(tài)高亮顯示示例

這是一個(gè)工作示例。

如果你運(yùn)行這段代碼,它將開始觀察下面的元素,并高亮顯示現(xiàn)在此處的所有代碼段:

let observer = new MutationObserver(mutations => {

  for(let mutation of mutations) {
    // 檢查新節(jié)點(diǎn),有什么需要高亮顯示的嗎?

    for(let node of mutation.addedNodes) {
      // 我們只跟蹤元素,跳過其他節(jié)點(diǎn)(例如文本節(jié)點(diǎn))
      if (!(node instanceof HTMLElement)) continue;

      // 檢查插入的元素是否為代碼段
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      }

      // 或者可能在子樹的某個(gè)地方有一個(gè)代碼段?
      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});

下面有一個(gè) HTML 元素,以及使用 innerHTML 動(dòng)態(tài)填充它的 JavaScript。

請先運(yùn)行前面那段代碼(上面那段,觀察元素),然后運(yùn)行下面這段代碼。你將看到 MutationObserver 是如何檢測并高亮顯示代碼段的。

下面這段代碼填充了其 innerHTML,這導(dǎo)致 MutationObserver 作出反應(yīng),并突出顯示其內(nèi)容:

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

// 動(dòng)態(tài)插入帶有代碼段的內(nèi)容
demoElem.innerHTML = `下面是一個(gè)代碼段:
  <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
  <div>另一個(gè)代碼段:</div>
  <div>
    <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
  </div>
`;

現(xiàn)在我們有了 MutationObserver,它可以跟蹤觀察到的元素中的,或者整個(gè) document 中的所有高亮顯示。我們可以在 HTML 中添加/刪除代碼段,而無需考慮高亮問題。

其他方法

有一個(gè)方法可以停止觀察節(jié)點(diǎn):

  • ?observer.disconnect()? —— 停止觀察。

當(dāng)我們停止觀察時(shí),觀察器可能尚未處理某些更改。在種情況下,我們使用:

  • ?observer.takeRecords()? —— 獲取尚未處理的變動(dòng)記錄列表,表中記錄的是已經(jīng)發(fā)生,但回調(diào)暫未處理的變動(dòng)。

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

// 如果你關(guān)心可能未處理的近期的變動(dòng)
// 那么,應(yīng)該在 disconnect 前調(diào)用獲取未處理的變動(dòng)列表
let mutationRecords = observer.takeRecords();

// 停止跟蹤變動(dòng)
observer.disconnect();
...

?observer.takeRecords()? 返回的記錄被從處理隊(duì)列中移除

回調(diào)函數(shù)不會(huì)被 observer.takeRecords() 返回的記錄調(diào)用。

垃圾回收

觀察器在內(nèi)部對節(jié)點(diǎn)使用弱引用。也就是說,如果一個(gè)節(jié)點(diǎn)被從 DOM 中移除了,并且該節(jié)點(diǎn)變得不可訪問,那么它就可以被垃圾回收。

觀察到 DOM 節(jié)點(diǎn)這一事實(shí)并不能阻止垃圾回收。

總結(jié)

MutationObserver 可以對 DOM 的變化作出反應(yīng) —— 特性(attribute),文本內(nèi)容,添加/刪除元素。

我們可以用它來跟蹤代碼其他部分引入的更改,以及與第三方腳本集成。

MutationObserver 可以跟蹤任何更改。config “要觀察的內(nèi)容”選項(xiàng)用于優(yōu)化,避免不必要的回調(diào)調(diào)用以節(jié)省資源。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號