瀏覽器知識點整理(十四)瀏覽器是怎麼監聽 DOM 的變化的?

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!html

前言

瀏覽器經過事件循環機制使頁面「活」起來,在事件循環中宏任務和微任務有不一樣的執行時機,而瀏覽器基於微任務的技術有 MutationObserverPromise 以及以 Promise 爲基礎開發出來的不少其餘的技術。前端

前面的文章也花了很大的篇幅介紹 Promise,那麼這篇文章就帶你們瞭解一下 MutationObserver 這個微任務是什麼?用來作什麼的吧!vue

MutationObserver 是用來監聽 DOM 變化的一套方法,而監聽 DOM 變化一直是前端工程師一項很是核心的需求。好比不少 Web 應用都利用 HTML 與 JavaScript 構建其自定義控件,與一些內置控件不一樣,這些控件不是固有的。爲了與內置控件一塊兒良好地工做,這些控件必須可以適應內容更改、響應事件和用戶交互。所以,Web 應用須要監視 DOM 變化並及時地作出響應react

MutationObserver 是如今監聽 DOM 變化的方法,那麼在開始的時候是怎麼監聽的呢,瞭解監聽 DOM 方法的演變有助於咱們更加深刻地理解瀏覽器是怎樣運行的。git

早期輪詢檢測

在早期,瀏覽器並無提供對監聽 DOM 的支持,因此那個時候要觀察 DOM 是否變化,惟一能作的即是 輪詢檢測,好比使用 setTimeout 或者 setInterval 來定時檢測 DOM 是否有改變。github

這種方式簡單粗暴,可是會遇到兩個問題:web

  • 若是時間間隔設置過長,DOM 變化 響應不夠及時
  • 反過來若是時間間隔設置太短,又會 浪費不少無用的工做量 去檢查 DOM,會讓頁面變得低效。

Mutation Event

在 2000 年的時候引入了 Mutation Event,它是在 DOM3 中定義的用於監聽 DOM 樹結構變化的事件,不過因爲該事件存在兼容性以及性能上的問題已經被棄用。後端

Mutation Event 總共有7種事件:DOMNodeInsertedDOMNodeRemovedDOMSubtreeModifiedDOMAttrModifiedDOMCharacterDataModifiedDOMNodeInsertedIntoDocumentDOMNodeRemovedFromDocument設計模式

簡單用法以下:數組

let box = document.getElementById('box')
box.addEventListener("DOMSubtreeModified", function () {
  console.log('box 元素被修改');
}, false);
複製代碼

Mutation Event 採用了 觀察者的設計模式,當 DOM 有變更時就會馬上觸發相應的事件,這種方式屬於 同步回調

採用 Mutation Event 解決了 實時性 的問題,由於 DOM 一旦發生變化,就會當即調用 JavaScript 接口。可是 這種實時性形成了嚴重的性能問題,由於每次 DOM 變更,渲染引擎都會去調用 JS,這樣會產生較大的性能開銷。

好比利用 JS 動態建立或動態修改 50 個節點內容,就會觸發 50 次回調,並且每一個回調函數都須要必定的執行時間,這裏咱們假設每次回調的執行時間是 4ms ,那麼 50 次回調的執行時間就是 200ms,若此時瀏覽器正在執行一個動畫效果,因爲 Mutation Event 觸發回調事件,就會致使動畫的卡頓。

也正是由於使用 Mutation Event 會致使頁面性能問題,因此 Mutation Event 被反對使用,並逐步從 Web 標準事件中刪除了。

MutationObserver

MutationObserver API 能夠用來監視 DOM 的變化,包括屬性的變化、節點的增減、內容的變化等。

MutationObserver 的使用

參考 MutationObserver 的 MDN 官方文檔資料

MutationObserver 是一個構造器,用來實例化一個 Mutation 觀察者對象,參數是一個回調函數,這個回調函數會在指定的 DOM 節點發送變化後執行,回調函數有兩個參數:

  • mutations:節點變化記錄數組(MutationRecord
  • observer:觀察者對象自己
let observe = new MutationObserver(function (mutations, observer) {});
複製代碼

MutationObserver 實例對象有三個方法,以下:

  • observe:配置 MutationObserver 在 DOM 更改匹配給定選項時,經過其回調函數開始接收通知。即設置觀察目標,接受兩個參數:
    • target:觀察目標;
    • options:經過對象成員來設置觀察選項
  • disconnect:阻止 MutationObserver 實例繼續接收的通知,直到再次調用其 observe() 方法,該觀察者對象包含的回調函數都不會再被調用。
  • takeRecords:從 MutationObserver 的通知隊列中刪除全部待處理的通知,並將它們返回到MutationRecord 對象的新 Array 中。即清空記錄隊列並返回裏面的內容。

使用實例:

// 選擇須要觀察變更的節點
const targetNode = document.getElementById('box');
// 觀察器的配置(須要觀察什麼變更)
const config = {
  attributes: true,
  childList: true,
  subtree: true
};
// 當觀察到變更時執行的回調函數
const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('有節點發生改變,當前節點的內容是:' + mutation.target.innerHTML);
    } else if (mutation.type === 'attributes') {
      console.log('修改了' + mutation.attributeName + '屬性');
    }
  }
};
// 建立一個觀察器實例並傳入回調函數
const observer = new MutationObserver(callback);
// 以上述配置開始觀察目標節點
observer.observe(targetNode, config);
// 以後,可中止觀察
// observer.disconnect();
複製代碼

MutationObserver 的改進優化

  • 首先,MutationObserver 將響應函數改爲異步調用,能夠不用在每次 DOM 變化都觸發異步調用,而是等屢次 DOM 變化後,一次觸發異步調用,而且還會使用一個數據結構來記錄這期間全部的 DOM 變化。這樣即便頻繁地操縱 DOM,也不會對性能形成太大的影響。
  • 在每次 DOM 節點發生變化的時候,渲染引擎將變化記錄封裝成微任務,並將微任務添加進當前的微任務隊列中。這樣當執行到檢查點的時候,V8 引擎就會按照順序執行微任務。

綜上所述,MutationObserver 採用了 異步 + 微任務 的策略來實現監聽 DOM 的變化。

  • 經過異步操做解決了同步操做的性能問題
  • 經過微任務解決了實時性的問題

MutationObserver 和 Vue 中的 nextTick

Vue 中 nextTick 可讓咱們在下次 DOM 更新循環結束以後執行延遲迴調,用於得到更新後的 DOM。

那在 Vue 中是怎麼實現 nextTick 的呢?

Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。

而異步回調咱們知道有宏任務(macrotasks)和微任務(microtasks)兩種,那爲了讓 nextTick 更快的執行,那確定是優先選擇微任務(microtasks)的。要建立一個新的微任務(microtask),會優先使用 Promise,若是瀏覽器不支持,再嘗試 MutationObserver。實在不支持,就只能用 setTimeout 這個宏任務了。

Vue 中的異步更新隊列 是這樣說的:

image.png

至於 MutationObserver 是怎麼模擬 nextTick 的,能夠看 源碼,其實就是建立一個 TextNode 並監聽內容變化,而後要 nextTick 的時候去改一下這個節點的文本內容:

var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
複製代碼

總結

這篇文章介紹了監聽 DOM 變化技術方案的演化史,從輪詢到 Mutation Event 再到最新使用的 MutationObserverMutationObserver 方案的核心就是採用微任務機制,有效地權衡了實時性和執行效率的問題

最後還簡單介紹了 MutationObserver 和 Vue 中 nextTick 的關係。

相關文章
相關標籤/搜索