MutationObserver 監聽 DOM 樹變化

MutationObserver 是用於代替 MutationEvents 做爲觀察 DOM 樹結構發生變化時,作出相應處理的 API 。爲何要使用 MutationObserver 去代替 MutationEvents 呢,咱們先了解一下 MutationEventshtml

MutationEvents

它簡單的用法以下:node

document.getElementById('list').addEventListener(
  'DOMSubtreeModified',
  () => {
    console.log('列表中子元素被修改')
  },
  false
)
// Mutation 事件列表
DOMAttrModified // 監聽元素的修改
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted // 監聽新增
DOMNodeRemoved // 監聽刪除
DOMNodeInsertedIntoDocument
DOMSubtreeModified // 監聽子元素的修改

其中 DOMNodeRemovedDOMNodeInsertedDOMSubtreeModified分別用於監聽元素子項的刪除,新增,修改(包括刪除和新增),DOMAttrModified 是監聽元素屬性的修改,而且可以提供具體的修改動做。數組

Mutation Events 遇到的問題瀏覽器

  • IE9 不支持 MutationEvents。Webkit 內核不支持 DOMAttrModified 特性,DOMElementNameChangedDOMAttributeNameChanged 在 Firefox 上不被支持。
  • 性能問題 1. MutationEvents 是同步執行的,它的每次調用,都須要從事件隊列中取出事件,執行,而後事件隊列中移除,期間須要移動隊列元素。若是事件觸發的較爲頻繁的話,每一次都須要執行上面的這些步驟,那麼瀏覽器會被拖慢。 2. MutationEvents 自己是事件,因此捕獲是採用的是事件冒泡的形式,若是冒泡捕獲期間又觸發了其餘的 MutationEvents 的話,頗有可能就會致使阻塞 Javascript 線程,甚至致使瀏覽器崩潰。

Mutation Observer

MutationObserver 是在 DOM4 中定義的,用於替代 MutationEvents 的新 API,它的不一樣於 events 的是,全部監聽操做以及相應處理都是在其餘腳本執行完成以後異步執行的,而且是因此變更觸發以後,將變得記錄在數組中,統一進行回調的,也就是說,當你使用 observer 監聽多個 DOM 變化時,而且這若干個 DOM 發生了變化,那麼 observer 會將變化記錄到變化數組中,等待一塊兒都結束了,而後一次性的從變化數組中執行其對應的回調函數。app

特色dom

  • 全部腳本任務完成後,纔會運行,即採用異步方式
  • DOM 變更記錄封裝成一個數組進行處理,而不是一條條地個別處理 DOM 變更。
  • 能夠觀察發生在 DOM 節點的全部變更,也能夠觀察某一類變更

目前,Firefox(14+)、Chrome(26+)、Opera(15+)、IE(11+) 和 Safari(6.1+) 支持這個 API。 Safari 6.0 和 Chrome 18-25 使用這個 API 的時候,須要加上 WebKit 前綴(WebKitMutationObserver)。可使用下面的表達式檢查瀏覽器是否支持這個 API。異步

const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 監測瀏覽器是否支持
const observeMutationSupport = !!MutationObserver

如何使用 MutationObserver

在應用中集成 MutationObserver 是至關簡單的。經過往構造函數 MutationObserver 中傳入一個函數做爲參數來初始化一個 MutationObserver 實例,該函數會在每次發生 DOM 發生變化的時候調用。MutationObserver 的函數的第一個參數即爲單個批處理中的 DOM 變化集。每一個變化包含了變化的類型和所發生的更改。編輯器

const mutationObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    console.log(mutation)
  })
})

建立的實例對象擁有三個方法:函數

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

observer 方法

observer 方法指定所要觀察的 DOM 元素,以及要觀察的特定變更。性能

const article = document.querySelector('article')
observer.observer(article, {
  childList: true,
  arrtibutes: true
})

上面代碼分析:

  1. 指定所要觀察的 DOM 元素 article
  2. 指定所要觀察的變更是子元素的變更和屬性變更。
  3. 將這兩個限定條件做爲參數,傳入observer 對象 observer方法。

disconnect 方法

  • disconnect 方法用來中止觀察。發生相應變更時,再也不調用回調函數。
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 選擇目標節點
const target = document.querySelector('#some-id')
// 建立觀察者對象
const observer = new MutationObserver(mutation =>  {
  mutations.forEach(function(mutation) {
    console.log(mutation.type)
  })
})
// 配置觀察選項:
const config = { attributes: true, childList: true, characterData: true }
// 傳入目標節點和觀察選項
observer.observe(target, config)
// 隨後,你還能夠中止觀察
observer.disconnect()

takeRecord 方法

takeRecord 方法用來清除變更記錄,即再也不處理未處理的變更。

在觀察者對象上調用 takeRecords 會返回 其觀察節點上的變化記錄(MutationRecord)數組。其中 MutationRecord 數組也會做爲,觀察者初始化時的回調函數的第一個參數。

其包含的屬性以下:

  • type 若是是屬性發生變化,則返回 attributes.若是是一個CharacterData 節點發生變化,則返回 characterData ,若是是目標節點的某個子節點發生了變化,則返回 childList .
  • target 返回這次變化影響到的節點,具體返回那種節點類型是根據 type 值的不一樣而不一樣的,若是 type 爲 attributes ,則返回發生變化的屬性節點所在的元素節點,若是 type 值爲 characterData ,則返回發生變化的這個 characterData 節點.若是 type 爲 childList ,則返回發生變化的子節點的父節點.
  • addedNodes 返回被添加的節點
  • removedNodes 返回被刪除的節點
  • previousSibling 返回被添加或被刪除的節點的前一個兄弟節點
  • nextSibling 返回被添加或被刪除的節點的後一個兄弟節點
  • attributeName 返回變動屬性的本地名稱
  • oldValue 根據 type 值的不一樣,返回的值也會不一樣.若是 type 爲 attributes,則返回該屬性變化以前的屬性值.若是 type 爲 characterData,則返回該節點變化以前的文本數據.若是 type 爲 childList,則返回 null
observer.takeRecord()

MutationObserver 類型

MutationObserver 所觀察的 DOM 變更(即上面代碼的 option 對象),包含如下類型:

  • childList:子元素的變更
  • attributes:屬性的變更
  • characterData:節點內容或節點文本的變更
  • subtree:全部下屬節點(包括子節點和子節點的子節點)的變更
想要觀察哪種變更類型,就在 option 對象中指定它的值爲 true。
須要注意的是,不能單獨觀察 subtree 變更,必須同時指定 childList、attributes 和 characterData 中的一種或多種。

除了變更類型,option 對象還能夠設定如下屬性:

  • attributeOldValue:值爲 true 或者爲 false。若是爲 true,則表示須要記錄變更前的屬性值。
  • characterDataOldValue:值爲 true 或者爲 false。若是爲 true,則表示須要記錄變更前的數據值。
  • attributesFilter:值爲一個數組,表示須要觀察的特定屬性(好比['class', 'str'])。

建立 MutationObserver 並 獲取 dom 元素,定義回調數據。

// 獲取MutationObserver,兼容低版本的瀏覽器
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 獲取dom元素
const list = document.querySelector('ol')
// 建立Observer
const Observer = new MutationObserver((mutations, instance) => {
  console.log(mutations)
  console.log(instance)
  mutations.forEach(mutation => {
    console.log(mutation)
  })
})
  • 子元素的變更
Observer.observe(list, {
  childList: true,
  subtree: true
})
// 追加div標籤
list.appendChild(document.createElement('div'))
// 追加文本
list.appendChild(document.createTextNode('foo'))
// 移除第一個節點
list.removeChild(list.childNodes[0])
// 子節點移除建立的div
list.childNodes[0].appendChild(document.createElement('div'))
  • 監測 characterData 的變更
Observer.observe(list, {
  childList: true,
  characterData: true,
  subtree: true
})
// 將第一個子節點的數據改成cha
list.childNodes[0].data = 'cha'
  • 監測屬性的變更
Observer.observe(list, {
  attributes: true
})
// 設置節點的屬性  會觸發回調函數
list.setAttribute('data-value', '111')
// 從新設置屬性 會觸發回調
list.setAttribute('data-value', '2222')
// 刪除屬性 也會觸發回調
list.removeAttribute('data-value')
  • 屬性變更前,記錄變更以前的值
Observer.observe(list, {
  attributes: true,
  attributeOldValue: true
})
// 設置節點的屬性  會觸發回調函數
list.setAttribute('data-value', '111')
// 刪除屬性
list.setAttribute('data-value', '2222')
  • characterData 變更時,記錄變更前的值。
Observer.observe(list, {
  childList: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
// 設置數據 觸發回調
list.childNodes[0].data = 'aaa'
// 從新設置數據 從新觸發回調
list.childNodes[0].data = 'bbbb'
  • attributeFilter {Array} 表示須要觀察的特定屬性 好比 ['class', 'src'];
Observer.observe(list, {
  attributes: true,
  attributeFilter: ['data-value']
})
// 第一次設置屬性 data-key 不會觸發的,由於data-value 不存在
list.setAttribute('data-key', 1)
// 第二次會觸發
list.setAttribute('data-value', 1)

案例分析—demo 編輯器

下面咱們作一個簡單的 demo 編輯器:

  1. 首先給父級元素 ol 設置 contenteditable 讓容器可編輯;
  2. 而後構造一個 observer 監聽子元素的變化;
  3. 每次回車的時候,控制檯輸出它的內容;
<div id="demo">
  <ol contenteditable style="border: 1px solid red">
    <li>111111</li>
  </ol>
</div>
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const list = document.querySelector('ol')
const Observer = new MutationObserver((mutations, instance) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      const list_values = [].slice
        .call(list.children)
        .map(node => node.innerHTML)
        .filter(s => s !== '<br>')
      console.log(list_values)
    }
  })
})
Observer.observe(list, {
  childList: true
})

如今咱們繼續能夠作一個相似於 input 和 textarea 中的 valueChange 的事件同樣的,監聽值變化,以前的值和以後的值,以下代碼:

const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const list = document.querySelector('ol')
const Observer = new MutationObserver((mutations, instance) => {
  mutations.forEach(mutation => {
    const enter = {
      mutation: mutation,
      el: mutation.target,
      newValue: mutation.target.textContent,
      oldValue: mutation.oldValue
    }
    console.log(enter)
  })
})

Observer.observe(list, {
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
注意: 對 input 和 textarea 不起做用的。

案例分析—編輯器統計字數

<div
  id="editor"
  contenteditable
  style="width: 240px; height: 80px; border: 1px solid red;"
></div>
<p id="textInput">還能夠輸入100字</p>
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const editor = document.querySelector('#editor')
const textInput = document.querySelector('#textInput')
const observer = new MutationObserver(mutations => {
  mutations.forEach(function(mutation) {
    if (mutation.type === 'characterData') {
      const newValue = mutation.target.textContent
      textInput.innerHTML = `還能夠輸入${1000 - newValue.length}字`
    }
  })
})
observer.observe(editor, {
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
相關文章
相關標籤/搜索