了解 MutationObserver API

已發表: 2022-03-10
快速總結↬在復雜的 Web 應用程序和框架中有時需要監控 DOM 的更改。 通過解釋和交互式演示,本文將向您展示如何使用 MutationObserver API 使觀察 DOM 變化變得相對容易。

在復雜的 Web 應用程序中,DOM 更改可能很頻繁。 因此,在某些情況下,您的應用可能需要響應對 DOM 的特定更改。

一段時間以來,尋找 DOM 更改的公認方法是通過稱為 Mutation Events 的功能,該功能現已棄用。 W3C 批准的 Mutation Events 替代品是 MutationObserver API,我將在本文中詳細討論它。

許多較早的文章和參考資料討論了為什麼舊功能被替換,所以我不會在這裡詳細介紹(除了我無法做到公正的事實)。 MutationObserver API 具有近乎完整的瀏覽器支持,因此如果需要,我們可以在大多數(如果不是全部)項目中安全地使用它。

MutationObserver 的基本語法

MutationObserver可以以多種不同的方式使用,我將在本文的其餘部分詳細介紹,但MutationObserver的基本語法如下所示:

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

第一行使用MutationObserver()構造函數創建一個新的MutationObserver 。 傳遞給構造函數的參數是一個回調函數,將在每個符合條件的 DOM 更改時調用。

確定什麼符合特定觀察者的方法是通過上述代碼中的最後一行。 在那條線上,我使用MutationObserverobserve()方法開始觀察。 您可以將其與addEventListener()類的內容進行比較。 一旦你附加了一個監聽器,頁面就會“監聽”指定的事件。 同樣,當您開始觀察時,頁面將開始“觀察”指定的MutationObserver

跳躍後更多! 繼續往下看↓

observe()方法有兩個參數: target ,應該是觀察變化的節點或節點樹; 和一個選項對象,它是一個MutationObserverInit對象,允許您為觀察者定義配置。

MutationObserver的最後一個關鍵基本特性是disconnect()方法。 這允許您停止觀察指定的更改,它看起來像這樣:

 observer.disconnect();

配置 MutationObserver 的選項

如前所述, MutationObserverobserve()方法需要第二個參數,該參數指定描述MutationObserver的選項。 以下是包含所有可能的屬性/值對的選項對象的外觀:

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

在設置MutationObserver選項時,沒有必要包括所有這些行。 我將這些僅用於參考目的,因此您可以查看可用的選項以及它們可以採用的值類型。 正如你所看到的,除了一個之外,所有的都是布爾值。

為了使MutationObserver工作,至少需要將childListattributescharacterData之一設置為true ,否則將引發錯誤。 其他四個屬性與這三個屬性之一結合使用(稍後會詳細介紹)。

到目前為止,我只是掩蓋了語法給您一個概述。 考慮這些功能如何工作的最佳方式是提供包含不同選項的代碼示例和現場演示。 這就是我將在本文的其餘部分做的事情。

使用 childList 觀察子元素的變化

您可以啟動的第一個也是最簡單的MutationObserver是查找要添加或刪除的指定節點(通常是元素)的子節點。 對於我的示例,我將在我的 HTML 中創建一個無序列表,並且我想知道何時從該列表元素中添加或刪除子節點。

列表的 HTML 如下所示:

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

我的MutationObserver的 JavaScript 包括以下內容:

 let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);

這只是代碼的一部分。 為簡潔起見,我將展示處理MutationObserver API 本身的最重要部分。

請注意我是如何循環遍歷mutations參數的,它是一個具有許多不同屬性的MutationRecord對象。 在這種情況下,我正在讀取type屬性並記錄一條消息,指示瀏覽器檢測到符合條件的突變。 另外,請注意我是如何將mList元素(對我的 HTML 列表的引用)作為目標元素(即我想要觀察其變化的元素)傳遞的。

  • 查看完整的交互式演示 →

使用按鈕啟動和停止MutationObserver 。 日誌消息有助於澄清正在發生的事情。 代碼中的註釋也提供了一些解釋。

請注意這裡的幾個要點:

  • 回調函數(我將其命名為mCallback ,以說明您可以隨意命名它)將在每次檢測到成功的突變時以及在執行observe()方法之後觸發。
  • 在我的示例中,唯一符合條件的突變“類型”是childList ,因此在循環遍歷 MutationRecord 時尋找這個是有意義的。 在這種情況下尋找任何其他類型都不會做任何事情(其他類型將在後續演示中使用)。
  • 使用childList ,我可以從目標元素中添加或刪除文本節點,這也符合條件。 因此,它不必是添加或刪除的元素。
  • 在此示例中,只有直接子節點才有資格。 在本文後面,我將向您展示如何將其應用於所有子節點、孫子節點等。

觀察元素屬性的變化

您可能想要跟踪的另一種常見類型的突變是指定元素上的屬性發生更改時。 在下一個交互式演示中,我將觀察段落元素屬性的變化。

 let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
  • 試用演示 →

同樣,為了清楚起見,我對代碼進行了縮寫,但重要的部分是:

  • options對象使用attributes屬性,設置為true以告訴MutationObserver我要查找對目標元素屬性的更改。
  • 我在循環中測試的突變類型是attributes ,在這種情況下唯一符合條件的突變類型。
  • 我還使用了mutation對象的attributeName屬性,它可以讓我找出更改了哪個屬性。
  • 當我觸發觀察者時,我通過引用傳遞段落元素以及選項。

在此示例中,一個按鈕用於切換目標 HTML 元素上的類名。 每次添加或刪除類時都會觸發突變觀察器中的回調函數。

觀察字符數據變化

您可能希望在您的應用程序中尋找的另一個變化是字符數據的突變; 也就是說,更改特定的文本節點。 這是通過在options對像中將characterData屬性設置為true來完成的。 這是代碼:

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

再次注意,在回調函數中查找的typecharacterData

  • 觀看現場演示 →

在此示例中,我正在尋找對特定文本節點的更改,我通過element.childNodes[0]定位該節點。 這有點hacky,但它適用於這個例子。 文本是用戶可通過段落元素上的contenteditable屬性進行編輯的。

觀察字符數據變化時的挑戰

如果您使用過contenteditable ,那麼您可能會知道有允許編輯富文本的鍵盤快捷鍵。 例如,CTRL-B 使文本變為粗體,CTRL-I 使文本變為斜體,等等。 這會將文本節點分解為多個文本節點,因此您會注意到MutationObserver將停止響應,除非您編輯仍被視為原始節點一部分的文本。

我還應該指出,如果您刪除所有文本, MutationObserver將不再觸發回調。 我假設發生這種情況是因為一旦文本節點消失,目標元素就不再存在。 為了解決這個問題,我的演示在刪除文本時停止觀察,儘管當您使用富文本快捷方式時事情會變得有點棘手。

但別擔心,在本文後面,我將討論一種更好的方式來使用characterData選項,而不必處理這些怪癖。

觀察指定屬性的變化

早些時候,我向您展示瞭如何觀察指定元素上屬性的變化。 在這種情況下,雖然演示會觸發類名更改,但我可以更改指定元素上的任何屬性。 但是,如果我想觀察一個或多個特定屬性的變化而忽略其他屬性怎麼辦?

我可以使用option對像中的可選attributeFilter屬性來做到這一點。 這是一個例子:

 let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }

如上所示, attributeFilter屬性接受我要監視的特定屬性數組。 在此示例中,每次修改hiddencontenteditabledata-par屬性中的一個或多個時, MutationObserver都會觸發回調。

  • 觀看現場演示 →

我再次針對特定的段落元素。 請注意選擇要更改的屬性的下拉菜單。 draggable屬性是唯一不符合條件的屬性,因為我沒有在選項中指定該屬性。

請注意,在代碼中,我再次使用MutationRecord對象的attributeName屬性來記錄更改了哪個屬性。 當然,與其他演示一樣,在單擊“開始”按鈕之前, MutationObserver不會開始監視更改。

我應該在這裡指出的另一件事是,在這種情況下,我不需要將attributes值設置為true 。 由於attributesFilter被設置為 true,這是隱含的。 這就是為什麼我的選項對象可能如下所示,並且它的工作方式相同:

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

另一方面,如果我將attributesattributeFilter數組一起顯式設置為false ,它將不起作用,因為false值將優先,並且過濾器選項將被忽略。

觀察節點及其子樹的變化

到目前為止,在設置每個MutationObserver時,我只處理目標元素本身,在childList的情況下,是元素的直接子元素。 但肯定有一種情況,我可能想觀察以下其中一項的變化:

  • 一個元素及其所有子元素;
  • 一個元素及其子元素的一個或多個屬性;
  • 元素內的所有文本節點。

以上所有都可以使用選項對象的subtree屬性來實現。

childList 帶子樹

首先,讓我們看看元素子節點的變化,即使它們不是直接子節點。 我可以將我的選項對象更改為如下所示:

 options = { childList: true, subtree: true }

代碼中的其他所有內容都或多或少與前面的childList示例相同,還有一些額外的標記和按鈕。

  • 觀看現場演示 →

這裡有兩個列表,一個嵌套在另一個列表中。 當MutationObserver啟動時,回調將觸發對任一列表的更改。 但是如果我將subtree屬性改回false (不存在時的默認值),則在修改嵌套列表時回調將不會執行。

帶有子樹的屬性

這是另一個示例,這次使用帶有attributesattributeFiltersubtree 。 這使我不僅可以觀察目標元素的屬性更改,還可以觀察目標元素的任何子元素的屬性的更改:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • 觀看現場演示 →

這與之前的屬性演示類似,但這次我設置了兩個不同的選擇元素。 第一個修改目標段落元素的屬性,而另一個修改段落內子元素的屬性。

同樣,如果您將subtree選項設置回false (或刪除它),第二個切換按鈕將不會觸發MutationObserver回調。 而且,當然,我可以完全省略attributeFilter ,而MutationObserver會查找對子樹中任何屬性的更改,而不是指定的屬性。

characterData 帶子樹

請記住,在之前的characterData演示中,目標節點消失以及MutationObserver不再工作時存在一些問題。 雖然有一些方法可以解決這個問題,但直接定位元素而不是文本節點更容易,然後使用subtree屬性來指定我希望該元素內的所有字符數據(無論它嵌套多深)觸發MutationObserver回調。

在這種情況下,我的選擇如下所示:

 options = { characterData: true, subtree: true }
  • 觀看現場演示 →

啟動觀察者後,嘗試使用 CTRL-B 和 CTRL-I 來設置可編輯文本的格式。 您會注意到這比前面的characterData示例更有效。 在這種情況下,分解的子節點不會影響觀察者,因為我們觀察的是目標節點內的所有節點,而不是單個文本節點。

記錄舊值

通常,在觀察 DOM 的變化時,您會想要記下舊值並可能將它們存儲或在其他地方使用它們。 這可以使用options對像中的幾個不同屬性來完成。

屬性舊值

首先,讓我們嘗試在舊屬性值更改後註銷它。 以下是我的選項與回調的外觀:

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • 觀看現場演示 →

注意MutationRecord對象的attributeNameoldValue屬性的使用。 通過在文本字段中輸入不同的值來嘗試演示。 請注意日誌如何更新以反映之前存儲的值。

字符數據舊值

同樣,如果我想記錄舊字符數據,我的選項如下所示:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • 觀看現場演示 →

請注意日誌消息指示先前的值。 當您通過富文本命令將 HTML 添加到組合中時,事情確實會變得有些不穩定。 我不確定在這種情況下正確的行為應該是什麼,但如果元素內唯一的東西是單個文本節點,它會更直接。

使用 takeRecords() 攔截突變

我還沒有提到的MutationObserver對象的另一種方法是takeRecords() 。 此方法允許您或多或少地攔截在回調函數處理之前檢測到的突變。

我可以使用這樣的行來使用此功能:

 let myRecords = observer.takeRecords();

這將 DOM 更改的列表存儲在指定變量中。 在我的演示中,只要單擊修改 DOM 的按鈕,我就會執行此命令。 請注意,開始和添加/刪除按鈕不記錄任何內容。 這是因為,如前所述,我在回調處理它們之前攔截了 DOM 更改。

但是請注意,我在停止觀察者的事件偵聽器中所做的事情:

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

如您所見,在使用observer.disconnect()停止觀察者後,我正在訪問被攔截的突變記錄,並且我正在記錄目標元素以及記錄的突變類型。 如果我一直在觀察多種類型的更改,那麼存儲的記錄中將包含多個項目,每個項目都有自己的類型。

當通過調用takeRecords()以這種方式截獲突變記錄時,通常會發送到回調函數的突變隊列被清空。 因此,如果由於某種原因您需要在處理這些記錄之前攔截它們, takeRecords()會派上用場。

使用單個觀察者觀察多個變化

請注意,如果我在頁面上的兩個不同節點上尋找突變,我可以使用同一個觀察者來完成。 這意味著在我調用構造函數之後,我可以對任意數量的元素執行observe()方法。

因此,在這一行之後:

 observer = new MutationObserver(mCallback);

然後,我可以使用不同的元素作為第一個參數進行多次observe()調用:

 observer.observe(mList, options); observer.observe(mList2, options);
  • 觀看現場演示 →

啟動觀察者,然後嘗試兩個列表的添加/刪除按鈕。 這裡唯一的問題是,如果您點擊“停止”按鈕之一,觀察者將停止觀察兩個列表,而不僅僅是它所針對的列表。

移動正在觀察的節點樹

我要指出的最後一件事是MutationObserver將繼續觀察對指定節點的更改,即使該節點已從其父元素中刪除。

例如,試試下面的演示:

  • 觀看現場演示 →

這是另一個使用childList監視目標元素的子元素更改的示例。 注意斷開子列表的按鈕,這是被觀察的。 單擊“開始...”按鈕,然後單擊“移動...”按鈕移動嵌套列表。 即使在列表從其父級中刪除後, MutationObserver仍會繼續觀察指定的更改。 發生這種情況並不令人意外,但需要牢記這一點。

結論

這幾乎涵蓋了MutationObserver API 的所有主要功能。 我希望本次深入探討對您熟悉此標準有所幫助。 如前所述,瀏覽器支持非常強大,您可以在 MDN 頁面上閱讀有關此 API 的更多信息。

我已將本文的所有演示放入 CodePen 集合中,如果您想有一個簡單的地方來擺弄演示。