JavaScript 是如何運作的:用 MutationObserver 追蹤 DOM 的變化
本系列專門研究 JavaScript 及其構建元件,這是第 10 期。在識別和描述核心元素的過程中,我們也分享了一些構建 SessionStack 的重要法則,SessionStack 是一個 JavaScript 應用,為了幫助使用者實時檢視和再現他們的 web 應用程式缺陷,它需要健壯並且高效能。
web 應用正在持續的越來越側重客戶端,這是由很多原因造成的,例如需要更豐富的 UI 來承載複雜應用的需求,實時運算,等等。
持續增加的複雜度使得在 web 應用的生命週期的任意時刻中獲取 UI 的確切狀態越來越困難。
而當你在搭建某些框架或者庫的時候,甚至會更加困難,例如,前者需要根據 DOM 來作出反應並執行特定的動作。
概覽
MutationObserver 是一個現代瀏覽器提供的 Web API,用於檢測 DOM 的變化。藉助這個 API,可以監聽到節點的新增或移除,節點的屬性變化或者字元節點的字元內容變化。
為什麼你會想要監聽 DOM?
這裡有很多 MutationObserver API 帶來極大便捷的例子,比如:
- 你想要提醒 web 應用的使用者,他現在所瀏覽的頁面有內容發生了變化。
- 你正在使用一個新的花哨的 JavaScript 框架,它根據 DOM 的變化動態載入 JavaScript 模組。
- 也許你正在開發一個 WYSIWYG 編輯器,並試著實現撤銷/重做功能。藉助 MutationObserver API,你在任何時間都能知道發生了什麼變化,所以撤銷也就非常容易。
這裡有幾個關於 MutationObserver 是如何帶來便捷的例子。
如何使用 MutationObserver
將 MutationObserver
應用於你的應用相當簡單。你需要通過傳入一個函式來建立一個 MutationObserver
例項,每當有變化發生,這個函式將會被呼叫。函式的第一個引數是一個批次內所有的變化(mutation)的集合。每個變化都會提供它的型別和已經發生的變化的資訊。
var mutationObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
複製程式碼
這個被建立的物件有三個方法:
observe
— 開始監聽變化。需要兩個引數 - 你需要觀察的 DOM 和一個設定物件disconnect
— 停止監聽變化takeRecords
— 在回撥函式呼叫之前,返回最後一個批次的變化。
下面這個程式碼片段展示瞭如何開始觀察:
// 開始監聽頁面中根 HTML 元素中的變化。
mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
複製程式碼
現在,假設在 DOM 中你有一些非常簡單的 div
:
<div id="sample-div" class="test"> Simple div </div>
複製程式碼
使用 jQuery,你可以移除這個 div 的 class
屬性:
$("#sample-div").removeAttr("class");
複製程式碼
當我們開始觀察,在呼叫 mutationObserver.observe(...)
之後我們將會在控制檯看到每個 MutationRecord 的日誌:
這個是由移除 class
屬性導致的變化。
最後,為了在任務結束後停止對 DOM 的觀察,你可以這樣做:
// 停止 MutationObserver 對變化的監聽。
mutationObserver.disconnect();
複製程式碼
現在,MutationObserver
已經被廣泛支援:
備擇方案
不管怎麼說,MutationObserver
並不是一直就有的。那麼當 MutationObserver
出現之前,開發者用的是什麼?
這是幾個可用的其他選項:
- Polling
- MutationEvents
- CSS animations
Polling
最簡單的最接近原生的方法是 polling。使用瀏覽器的 setInterval web 介面你可以設定一個在一段時間後檢查是否有變化發生的的任務。自然,這個方法將會嚴重的降低應用或者網站的效能。
MutationEvents
在 2000 年,MutationEvents API 被引入。儘管很有用,但是每次 DOM 發生變化 mutation events 都會被觸發,這將再次導致效能問題。現在 MutationEvents
介面已經被廢棄,很快,現代瀏覽器將會完全停止對它的支援。
這是瀏覽器對 MutationEvents
的支援:
CSS animations
一個有點奇怪的備選方案依賴於 CSS animations。這可能聽起來有些讓人困惑。基本上,這個方案是建立一個動畫,一旦一個元素被新增到了 DOM,這個動畫就將會被觸發。動畫開始的時候,animationstart
事件會被觸發:如果你對這個事件繫結了一個處理器,你將會確切的知道元素是什麼時候被新增到 DOM 的。動畫執行的時間段很短,所以實際應用的時候它對使用者是不可見的。
首先,我們需要一個父級元素,我們在它的內部監聽節點的插入:
<div id=”container-element”></div>
複製程式碼
為了得到節點插入的處理器,我們需要設定一系列的 keyframe 動畫,當節點插入的時候,動畫將會開始。
@keyframes nodeInserted {
from { opacity: 0.99; }
to { opacity: 1; }
}
複製程式碼
keyframes 已經建立,動畫還需要被應用於你想要監聽的元素。注意應設定很小的 duration 值 —— 它們將會減弱動畫在瀏覽器上留下的痕跡:
#container-element * {
animation-duration: 0.001s;
animation-name: nodeInserted;
}
複製程式碼
這為 container-element
的所有子節點都新增了動畫。當動畫結束後,插入的事件將會被觸發。
我們需要一個作為事件監聽者的 JavaScript 方法。在方法內部,必須確保初始的 event.animationName
檢測是我們想要的那個動畫。
var insertionListener = function(event) {
// 確保這是我們想要的那個動畫。
if (event.animationName === "nodeInserted") {
console.log("Node has been inserted: " + event.target);
}
}
複製程式碼
現在是時候為父級元素新增事件監聽了:
document.addEventListener(“animationstart”, insertionListener, false); // standard + firefox
document.addEventListener(“MSAnimationStart”, insertionListener, false); // IE
document.addEventListener(“webkitAnimationStart”, insertionListener, false); // Chrome + Safari
複製程式碼
這是瀏覽器對於 CSS 動畫的支援:
MutationObserver
能提供上述提到的解決方案沒有的很多優點。本質上,它能覆蓋到每一個可能發生的 DOM 的變化,並且它會在一個批次的變化發生後被觸發,這種方法使得它得到大大的優化。最重要的,MutationObserver
被所有的主流現代瀏覽器所支援,還有一些使用引擎下 MutationEvents 的 polyfill。
MutationObserver
在 SessionStack 庫中佔據了核心位置。
你一旦將 SessionStack 庫整合進 web 應用,它就開始收集 DOM 變化、網路請求、錯誤資訊、debug 資訊等等,併傳送到我們的伺服器。SessionStack 使用這些資訊重新建立了你的使用者端發生的一切,並以發生在使用者端的同樣的方式將產品的問題展現給你。很多使用者認為 SessionStack 記錄的實際是視訊 -- 然而它並沒有。記錄真實情況的視訊是很耗費資源的,然而我們收集的少量資料卻很輕量,並不會影響 UX 和你的 web 應用的效能。
如果你想要嘗試一下 SessionStack,這是一個免費的設計案例。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。