JavaScript Mutation Observer API

2023-03-20 15:47 更新

概述

Mutation Observer API 用來(lái)監(jiān)視 DOM 變動(dòng)。DOM 的任何變動(dòng),比如節(jié)點(diǎn)的增減、屬性的變動(dòng)、文本內(nèi)容的變動(dòng),這個(gè) API 都可以得到通知。

概念上,它很接近事件,可以理解為 DOM 發(fā)生變動(dòng)就會(huì)觸發(fā) Mutation Observer 事件。但是,它與事件有一個(gè)本質(zhì)不同:事件是同步觸發(fā),也就是說(shuō),DOM 的變動(dòng)立刻會(huì)觸發(fā)相應(yīng)的事件;Mutation Observer 則是異步觸發(fā),DOM 的變動(dòng)并不會(huì)馬上觸發(fā),而是要等到當(dāng)前所有 DOM 操作都結(jié)束才觸發(fā)。

這樣設(shè)計(jì)是為了應(yīng)付 DOM 變動(dòng)頻繁的特點(diǎn)。舉例來(lái)說(shuō),如果文檔中連續(xù)插入1000個(gè)<p>元素,就會(huì)連續(xù)觸發(fā)1000個(gè)插入事件,執(zhí)行每個(gè)事件的回調(diào)函數(shù),這很可能造成瀏覽器的卡頓;而 Mutation Observer 完全不同,只在1000個(gè)段落都插入結(jié)束后才會(huì)觸發(fā),而且只觸發(fā)一次。

Mutation Observer 有以下特點(diǎn)。

  • 它等待所有腳本任務(wù)完成后,才會(huì)運(yùn)行(即異步觸發(fā)方式)。
  • 它把 DOM 變動(dòng)記錄封裝成一個(gè)數(shù)組進(jìn)行處理,而不是一條條個(gè)別處理 DOM 變動(dòng)。
  • 它既可以觀察 DOM 的所有類(lèi)型變動(dòng),也可以指定只觀察某一類(lèi)變動(dòng)。

MutationObserver 構(gòu)造函數(shù)

使用時(shí),首先使用MutationObserver構(gòu)造函數(shù),新建一個(gè)觀察器實(shí)例,同時(shí)指定這個(gè)實(shí)例的回調(diào)函數(shù)。

var observer = new MutationObserver(callback);

上面代碼中的回調(diào)函數(shù),會(huì)在每次 DOM 變動(dòng)后調(diào)用。該回調(diào)函數(shù)接受兩個(gè)參數(shù),第一個(gè)是變動(dòng)數(shù)組,第二個(gè)是觀察器實(shí)例,下面是一個(gè)例子。

var observer = new MutationObserver(function (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});

MutationObserver 的實(shí)例方法

observe()

observe()方法用來(lái)啟動(dòng)監(jiān)聽(tīng),它接受兩個(gè)參數(shù)。

  • 第一個(gè)參數(shù):所要觀察的 DOM 節(jié)點(diǎn)
  • 第二個(gè)參數(shù):一個(gè)配置對(duì)象,指定所要觀察的特定變動(dòng)
var article = document.querySelector('article');

var  options = {
  'childList': true,
  'attributes':true
} ;

observer.observe(article, options);

上面代碼中,observe()方法接受兩個(gè)參數(shù),第一個(gè)是所要觀察的DOM元素是article,第二個(gè)是所要觀察的變動(dòng)類(lèi)型(子節(jié)點(diǎn)變動(dòng)和屬性變動(dòng))。

觀察器所能觀察的 DOM 變動(dòng)類(lèi)型(即上面代碼的options對(duì)象),有以下幾種。

  • childList:子節(jié)點(diǎn)的變動(dòng)(指新增,刪除或者更改)。
  • attributes:屬性的變動(dòng)。
  • characterData:節(jié)點(diǎn)內(nèi)容或節(jié)點(diǎn)文本的變動(dòng)。

想要觀察哪一種變動(dòng)類(lèi)型,就在option對(duì)象中指定它的值為true。需要注意的是,至少必須同時(shí)指定這三種觀察的一種,若均未指定將報(bào)錯(cuò)。

除了變動(dòng)類(lèi)型,options對(duì)象還可以設(shè)定以下屬性:

  • subtree:布爾值,表示是否將該觀察器應(yīng)用于該節(jié)點(diǎn)的所有后代節(jié)點(diǎn)。
  • attributeOldValue:布爾值,表示觀察attributes變動(dòng)時(shí),是否需要記錄變動(dòng)前的屬性值。
  • characterDataOldValue:布爾值,表示觀察characterData變動(dòng)時(shí),是否需要記錄變動(dòng)前的值。
  • attributeFilter:數(shù)組,表示需要觀察的特定屬性(比如['class','src'])。
// 開(kāi)始監(jiān)聽(tīng)文檔根節(jié)點(diǎn)(即<html>標(biāo)簽)的變動(dòng)
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});

對(duì)一個(gè)節(jié)點(diǎn)添加觀察器,就像使用addEventListener()方法一樣,多次添加同一個(gè)觀察器是無(wú)效的,回調(diào)函數(shù)依然只會(huì)觸發(fā)一次。如果指定不同的options對(duì)象,以后面添加的那個(gè)為準(zhǔn),類(lèi)似覆蓋。

下面的例子是觀察新增的子節(jié)點(diǎn)。

var insertedNodes = [];
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    for (var i = 0; i < mutation.addedNodes.length; i++) {
      insertedNodes.push(mutation.addedNodes[i]);
    }
  });
  console.log(insertedNodes);
});
observer.observe(document, { childList: true, subtree: true });

disconnect(),takeRecords()

disconnect()方法用來(lái)停止觀察。調(diào)用該方法后,DOM 再發(fā)生變動(dòng),也不會(huì)觸發(fā)觀察器。

observer.disconnect();

takeRecords()方法用來(lái)清除變動(dòng)記錄,即不再處理未處理的變動(dòng)。該方法返回變動(dòng)記錄的數(shù)組。

observer.takeRecords();

下面是一個(gè)例子。

// 保存所有沒(méi)有被觀察器處理的變動(dòng)
var changes = mutationObserver.takeRecords();

// 停止觀察
mutationObserver.disconnect();

MutationRecord 對(duì)象

DOM 每次發(fā)生變化,就會(huì)生成一條變動(dòng)記錄(MutationRecord 實(shí)例)。該實(shí)例包含了與變動(dòng)相關(guān)的所有信息。Mutation Observer 處理的就是一個(gè)個(gè)MutationRecord實(shí)例所組成的數(shù)組。

MutationRecord對(duì)象包含了DOM的相關(guān)信息,有如下屬性:

  • type:觀察的變動(dòng)類(lèi)型(attributes、characterData或者childList)。
  • target:發(fā)生變動(dòng)的DOM節(jié)點(diǎn)。
  • addedNodes:新增的DOM節(jié)點(diǎn)。
  • removedNodes:刪除的DOM節(jié)點(diǎn)。
  • previousSibling:前一個(gè)同級(jí)節(jié)點(diǎn),如果沒(méi)有則返回null。
  • nextSibling:下一個(gè)同級(jí)節(jié)點(diǎn),如果沒(méi)有則返回null。
  • attributeName:發(fā)生變動(dòng)的屬性。如果設(shè)置了attributeFilter,則只返回預(yù)先指定的屬性。
  • oldValue:變動(dòng)前的值。這個(gè)屬性只對(duì)attributecharacterData變動(dòng)有效,如果發(fā)生childList變動(dòng),則返回null

應(yīng)用示例

子元素的變動(dòng)

下面的例子說(shuō)明如何讀取變動(dòng)記錄。

var callback = function (records){
  records.map(function(record){
    console.log('Mutation type: ' + record.type);
    console.log('Mutation target: ' + record.target);
  });
};

var mo = new MutationObserver(callback);

var option = {
  'childList': true,
  'subtree': true
};

mo.observe(document.body, option);

上面代碼的觀察器,觀察<body>的所有下級(jí)節(jié)點(diǎn)(childList表示觀察子節(jié)點(diǎn),subtree表示觀察后代節(jié)點(diǎn))的變動(dòng)。回調(diào)函數(shù)會(huì)在控制臺(tái)顯示所有變動(dòng)的類(lèi)型和目標(biāo)節(jié)點(diǎn)。

屬性的變動(dòng)

下面的例子說(shuō)明如何追蹤屬性的變動(dòng)。

var callback = function (records) {
  records.map(function (record) {
    console.log('Previous attribute value: ' + record.oldValue);
  });
};

var mo = new MutationObserver(callback);

var element = document.getElementById('#my_element');

var options = {
  'attributes': true,
  'attributeOldValue': true
}

mo.observe(element, options);

上面代碼先設(shè)定追蹤屬性變動(dòng)('attributes': true),然后設(shè)定記錄變動(dòng)前的值。實(shí)際發(fā)生變動(dòng)時(shí),會(huì)將變動(dòng)前的值顯示在控制臺(tái)。

取代 DOMContentLoaded 事件

網(wǎng)頁(yè)加載的時(shí)候,DOM 節(jié)點(diǎn)的生成會(huì)產(chǎn)生變動(dòng)記錄,因此只要觀察 DOM 的變動(dòng),就能在第一時(shí)間觸發(fā)相關(guān)事件,也就沒(méi)有必要使用DOMContentLoaded事件。

var observer = new MutationObserver(callback);
observer.observe(document.documentElement, {
  childList: true,
  subtree: true
});

上面代碼中,監(jiān)聽(tīng)document.documentElement(即網(wǎng)頁(yè)的<html>HTML 節(jié)點(diǎn))的子節(jié)點(diǎn)的變動(dòng),subtree屬性指定監(jiān)聽(tīng)還包括后代節(jié)點(diǎn)。因此,任意一個(gè)網(wǎng)頁(yè)元素一旦生成,就能立刻被監(jiān)聽(tīng)到。

下面的代碼,使用MutationObserver對(duì)象封裝一個(gè)監(jiān)聽(tīng) DOM 生成的函數(shù)。

(function(win){
  'use strict';

  var listeners = [];
  var doc = win.document;
  var MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  var observer;

  function ready(selector, fn){
    // 儲(chǔ)存選擇器和回調(diào)函數(shù)
    listeners.push({
      selector: selector,
      fn: fn
    });
    if(!observer){
      // 監(jiān)聽(tīng)document變化
      observer = new MutationObserver(check);
      observer.observe(doc.documentElement, {
        childList: true,
        subtree: true
      });
    }
    // 檢查該節(jié)點(diǎn)是否已經(jīng)在DOM中
    check();
  }

  function check(){
  // 檢查是否匹配已儲(chǔ)存的節(jié)點(diǎn)
    for(var i = 0; i < listeners.length; i++){
      var listener = listeners[i];
      // 檢查指定節(jié)點(diǎn)是否有匹配
      var elements = doc.querySelectorAll(listener.selector);
      for(var j = 0; j < elements.length; j++){
        var element = elements[j];
        // 確保回調(diào)函數(shù)只會(huì)對(duì)該元素調(diào)用一次
        if(!element.ready){
          element.ready = true;
          // 對(duì)該節(jié)點(diǎn)調(diào)用回調(diào)函數(shù)
          listener.fn.call(element, element);
        }
      }
    }
  }

  // 對(duì)外暴露ready
  win.ready = ready;

})(this);

// 使用方法
ready('.foo', function(element){
  // ...
});

參考鏈接


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)