在某些場景下,咱們但願能監視 DOM 樹的變更,而後作一些相關的操做。好比監聽元素被插入 DOM 或從 DOM 樹中移除,而後添加相應的動畫效果。或者在富文本編輯器中輸入特殊的符號,如 #
或 @
符號時自動高亮後面的內容等。要實現這些功能,咱們就能夠考慮使用 MutationObserver API,接下來阿寶哥將帶你們一塊兒來探索 MutationObserver API 所提供的強大能力。javascript
閱讀完本文,你將瞭解如下內容:css
MutationObserver 接口提供了監視對 DOM 樹所作更改的能力。它被設計爲舊的 Mutation Events 功能的替代品,該功能是 DOM3 Events 規範的一部分。html
利用 MutationObserver API 咱們能夠監視 DOM 的變化。DOM 的任何變化,好比節點的增長、減小、屬性的變更、文本內容的變更,經過這個 API 咱們均可以獲得通知。前端
MutationObserver 有如下特色:vue
在介紹 MutationObserver API 以前,咱們先來了解一下它的兼容性:java
(圖片來源:https://caniuse.com/#search=M...)node
從上圖可知,目前主流的 Web 瀏覽器基本都支持 MutationObserver API,而對於 IE 瀏覽器只有 IE 11 才支持。在項目中,如須要使用 MutationObserver API,首先咱們須要建立 MutationObserver 對象,所以接下來咱們來介紹 MutationObserver 構造函數。git
DOM 規範中的 MutationObserver 構造函數,用於建立並返回一個新的觀察器,它會在觸發指定 DOM 事件時,調用指定的回調函數。MutationObserver 對 DOM 的觀察不會當即啓動,而必須先調用 observe()
方法來指定所要觀察的 DOM 節點以及要響應哪些更改。github
MutationObserver 構造函數的語法爲:web
const observer = new MutationObserver(callback);
相關的參數說明以下:
使用示例
const observer = new MutationObserver(function (mutations, observer) { mutations.forEach(function(mutation) { console.log(mutation); }); });
observe(target[, options]):該方法用來啓動監聽,它接受兩個參數。第一個參數,用於指定所要觀察的 DOM 節點。第二個參數,是一個配置對象,用於指定所要觀察的特定變更。
const editor = document.querySelector('#editor'); const options = { childList: true, // 監視node直接子節點的變更 subtree: true, // 監視node全部後代的變更 attributes: true, // 監視node屬性的變更 characterData: true, // 監視指定目標節點或子節點樹中節點所包含的字符數據的變化。 attributeOldValue: true // 記錄任何有改動的屬性的舊值 }; observer.observe(article, options);
DOM 每次發生變化,就會生成一條變更記錄,即 MutationRecord 實例。該實例包含了與變更相關的全部信息。Mutation Observer 對象處理的就是一個個 MutationRecord 實例所組成的數組。
MutationRecord 實例包含了變更相關的信息,含有如下屬性:
null
;null
;attributeFilter
,則只返回預先指定的屬性;attribute
和 characterData
變更有效,若是發生 childList
變更,則返回 null
。<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>DOM 變更觀察器示例</title> <style> .editor {border: 1px dashed grey; width: 400px; height: 300px;} </style> </head> <body> <h3>阿寶哥:DOM 變更觀察器(Mutation observer)</h3> <div contenteditable id="container" class="editor">你們好,我是阿寶哥!</div> <script> const containerEle = document.querySelector("#container"); let observer = new MutationObserver((mutationRecords) => { console.log(mutationRecords); // 輸出變更記錄 }); observer.observe(containerEle, { subtree: true, // 監視node全部後代的變更 characterDataOldValue: true, // 記錄任何有變更的屬性的舊值 }); </script> </body> </html>
以上代碼成功運行以後,阿寶哥對 id 爲 container 的 div 容器中原始內容進行修改,即把 你們好,我是阿寶哥! 修改成 你們好,我。對於上述的修改,控制檯將會輸出 5 條變更記錄,這裏咱們來看一下最後一條變更記錄:
MutationObserver 對象的 observe(target [, options])
方法支持不少配置項,這裏阿寶哥就不詳細展開介紹了。
可是爲了讓剛接觸 MutationObserver API 的小夥伴能更直觀的感覺每一個配置項的做用,阿寶哥把 mutationobserver-api-guide 這篇文章中使用的在線示例統一提取出來,作了一下彙總與分類:
一、 MutationObserver Example - childList: https://codepen.io/impressive...二、MutationObserver Example - childList with subtree:https://codepen.io/impressive...
三、MutationObserver Example - Attributes:https://codepen.io/impressive...
四、MutationObserver Example - Attribute Filter:https://codepen.io/impressive...
五、MutationObserver Example - attributeFilter with subtree:https://codepen.io/impressive...
六、MutationObserver Example - characterData:https://codepen.io/impressive...
七、MutationObserver Example - characterData with subtree:https://codepen.io/impressive...
八、MutationObserver Example - Recording an Old Attribute Value:https://codepen.io/impressive...
九、MutationObserver Example - Recording old characterData:https://codepen.io/impressive...
十、MutationObserver Example - Multiple Changes for a Single Observer:https://codepen.io/impressive...
十一、MutationObserver Example - Moving a Node Tree:https://codepen.io/impressive...
相信你們對語法高亮都不會陌生,平時在閱讀各種技術文章時,都會遇到它。接下來,阿寶哥將跟你們介紹如何使用 MutationObserver API 和 Prism.js 這個庫實現 JavaScript 和 CSS 語法高亮。
在看具體的實現代碼前,咱們先來看一下如下 HTML 代碼段未語法高亮和語法高亮的區別:
let htmlSnippet = `下面是一個JavaScript代碼段: <pre class="language-javascript"> <code> let greeting = "你們好,我是阿寶哥"; </code> </pre> <div>另外一個CSS代碼段:</div> <div> <pre class="language-css"> <code>#code-container { border: 1px dashed grey; padding: 5px; } </code> </pre> </div> `
經過觀察上圖,咱們能夠很直觀地發現,有進行語法高亮的代碼塊閱讀起來更加清晰易懂。下面咱們來看一下實現語法高亮的功能代碼:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>MutationObserver 實戰之語法高亮</title> <style> #code-container { border: 1px dashed grey; padding: 5px; width: 550px; height: 200px; } </style> <link href="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/themes/prism.min.css" rel="stylesheet"> <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/prism.min.js" data-manual></script> <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-javascript.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/prism/9000.0.1/components/prism-css.min.js"></script> </head> <body> <h3>阿寶哥:MutationObserver 實戰之語法高亮</h3> <div id="code-container"></div> <script> let observer = new MutationObserver((mutations) => { for (let mutation of mutations) { // 獲取新增的DOM節點 for (let node of mutation.addedNodes) { // 只處理HTML元素,跳過其餘節點,好比文本節點 if (!(node instanceof HTMLElement)) continue; // 檢查插入的節點是否爲代碼段 if (node.matches('pre[class*="language-"]')) { Prism.highlightElement(node); } // 檢查插入節點的子節點是否爲代碼段 for (let elem of node.querySelectorAll('pre[class*="language-"]')) { Prism.highlightElement(elem); } } } }); let codeContainer = document.querySelector("#code-container"); observer.observe(codeContainer, { childList: true, subtree: true }); // 動態插入帶有代碼段的內容 codeContainer.innerHTML = `下面是一個JavaScript代碼段: <pre class="language-javascript"><code> let greeting = "你們好,我是阿寶哥"; </code></pre> <div>另外一個CSS代碼段:</div> <div> <pre class="language-css"> <code>#code-container { border: 1px dashed grey; padding: 5px; } </code> </pre> </div> `; </script> </body> </html>
在以上代碼中,首先咱們在引入 prism.min.js 的 script 標籤上設置 data-manual
屬性,用於告訴 Prism.js 咱們將使用手動模式來處理語法高亮。接着咱們在回調函數中經過獲取 mutation 對象的 addedNodes
屬性來進一步獲取新增的 DOM 節點。而後咱們遍歷新增的 DOM 節點,判斷新增的 DOM 節點是否爲代碼段,若是知足條件的話則進行高亮操做。
此外,除了判斷當前節點以外,咱們也會判斷插入節點的子節點是否爲代碼段,若是知足條件的話,也會進行高亮操做。
對 Web 開發者來講,相信不少人對 load
事件都不會陌生。當整個頁面及全部依賴資源如樣式表和圖片都已完成加載時,將會觸發 load
事件。而當文檔或一個子資源正在被卸載時,會觸發 unload
事件。
在平常開發過程當中,除了監聽頁面的加載和卸載事件以外,咱們常常還須要監聽 DOM 節點的插入和移除事件。好比當 DOM 節點插入 DOM 樹中產生插入動畫,而當節點從 DOM 樹中被移除時產生移除動畫。針對這種場景咱們就能夠利用 MutationObserver API 來監聽元素的添加與移除。
一樣,在看具體的實現代碼前,咱們先來看一下實際的效果:
在以上示例中,當點擊 跟蹤元素生命週期 按鈕時,一個新的 DIV 元素會被插入到 body 中,成功插入後,會在消息框顯示相關的信息。在 3S 以後,新增的 DIV 元素會從 DOM 中移除,成功移除後,會在消息框中顯示 元素已從DOM中移除了 的信息。
下面咱們來看一下具體實現:
index.html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>MutationObserver load/unload 事件</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.0.0/animate.min.css" /> </head> <body> <h3>阿寶哥:MutationObserver load/unload 事件</h3> <div class="block"> <p> <button onclick="trackElementLifecycle()">跟蹤元素生命週期</button> </p> <textarea id="messageContainer" rows="5" cols="50"></textarea> </div> <script src="./on-load.js"></script> <script> const busy = false; const messageContainer = document.querySelector("#messageContainer"); function trackElementLifecycle() { if (busy) return; const div = document.createElement("div"); div.innerText = "我是新增的DIV元素"; div.classList.add("animate__animated", "animate__bounceInDown"); watchElement(div); document.body.appendChild(div); } function watchElement(element) { onload( element, function (el) { messageContainer.value = "元素已被添加到DOM中, 3s後將被移除"; setTimeout(() => document.body.removeChild(el), 3000); }, function (el) { messageContainer.value = "元素已從DOM中移除了"; } ); } </script> </body> </html>
on-load.js
// 只包含部分代碼 const watch = Object.create(null); const KEY_ID = "onloadid" + Math.random().toString(36).slice(2); const KEY_ATTR = "data-" + KEY_ID; let INDEX = 0; if (window && window.MutationObserver) { const observer = new MutationObserver(function (mutations) { if (Object.keys(watch).length < 1) return; for (let i = 0; i < mutations.length; i++) { if (mutations[i].attributeName === KEY_ATTR) { eachAttr(mutations[i], turnon, turnoff); continue; } eachMutation(mutations[i].removedNodes, function (index, el) { if (!document.documentElement.contains(el)) turnoff(index, el); }); eachMutation(mutations[i].addedNodes, function (index, el) { if (document.documentElement.contains(el)) turnon(index, el); }); } }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeOldValue: true, attributeFilter: [KEY_ATTR], }); } function onload(el, on, off, caller) { on = on || function () {}; off = off || function () {}; el.setAttribute(KEY_ATTR, "o" + INDEX); watch["o" + INDEX] = [on, off, 0, caller || onload.caller]; INDEX += 1; return el; }
on-load.js 的完整代碼: https://gist.github.com/semli...
除了前面兩個應用場景,在富文本編輯器的場景,MutationObserver API 也有它的用武之地。好比咱們但願在富文本編輯器中高亮 #
符號後的內容,這時候咱們就能夠經過 MutationObserver API 來監聽用戶輸入的內容,發現用戶輸入 #
時自動對輸入的內容進行高亮處理。
這裏阿寶哥基於 vue-hashtag-textarea 這個項目來演示一下上述的效果:
此外,MutationObserver API 在 Github 上的一個名爲 Editor.js 的項目中也有應用。Editor.js 是一個 Block-Styled 編輯器,以 JSON 格式輸出數據的富文本和媒體編輯器。它是徹底模塊化的,由 「塊」 組成,這意味着每一個結構單元都是它本身的塊(例如段落、標題、圖像都是塊),用戶能夠輕鬆地編寫本身的插件來進一步擴展編輯器。
在 Editor.js 編輯器內部,它經過 MutationObserver API 來監聽富文本框的內容異動,而後觸發 change 事件,使得外部能夠對變更進行響應和處理。上述的功能被封裝到內部的 modificationsObserver.ts 模塊,感興趣的小夥伴能夠閱讀 modificationsObserver.ts 模塊的代碼。
固然利用 MutationObserver API 提供的強大能力,咱們還能夠有其餘的應用場景,好比防止頁面的水印元素被刪除,從而避免沒法跟蹤到 「泄密」 者,固然這並非絕對的安全,只是多加了一層防禦措施。具體如何實現水印元素被刪除,篇幅有限。這裏阿寶哥不繼續展開介紹了,你們能夠參考掘金上 「打開控制檯也刪不掉的元素,前端都嚇尿了」 這一篇文章。
至此 MutationObserver 變更觀察者相關內容已經介紹完了,既然講到觀察者,阿寶哥不由自主想再介紹一下觀察者設計模式。
觀察者模式,它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生變化時就會通知全部的觀察者對象,使得它們可以自動更新本身。
咱們可使用平常生活中,期刊訂閱的例子來形象地解釋一下上面的概念。期刊訂閱包含兩個主要的角色:期刊出版方和訂閱者,他們之間的關係以下:
在觀察者模式中也有兩個主要角色:Subject(主題)和 Observer(觀察者),它們分別對應例子中的期刊出版方和訂閱者。接下來咱們來看張圖,進一步加深對以上概念的理解。
觀察者模式包含如下角色:
interface Observer { notify: Function; }
class ConcreteObserver implements Observer{ constructor(private name: string) {} notify() { console.log(`${this.name} has been notified.`); } }
class Subject { private observers: Observer[] = []; public addObserver(observer: Observer): void { console.log(observer, "is pushed!"); this.observers.push(observer); } public deleteObserver(observer: Observer): void { console.log("remove", observer); const n: number = this.observers.indexOf(observer); n != -1 && this.observers.splice(n, 1); } public notifyObservers(): void { console.log("notify all the observers", this.observers); this.observers.forEach(observer => observer.notify()); } }
const subject: Subject = new Subject(); const semlinker = new ConcreteObserver("semlinker"); const kaquqo = new ConcreteObserver("kakuqo"); subject.addObserver(semlinker); subject.addObserver(kaquqo); subject.notifyObservers(); subject.deleteObserver(kaquqo); subject.notifyObservers();
以上代碼成功運行後,控制檯會輸出如下結果:
[LOG]: { "name": "semlinker" }, is pushed! [LOG]: { "name": "kakuqo" }, is pushed! [LOG]: notify all the observers, [ { "name": "semlinker" }, { "name": "kakuqo" } ] [LOG]: semlinker has been notified. [LOG]: kakuqo has been notified. [LOG]: remove, { "name": "kakuqo" } [LOG]: notify all the observers, [ { "name": "semlinker" } ] [LOG]: semlinker has been notified.
經過觀察以上的輸出結果,當觀察者被移除之後,後續的通知就接收不到了。觀察者模式支持簡單的廣播通訊,可以自動通知全部已經訂閱過的對象。但若是一個被觀察者對象有不少的觀察者的話,將全部的觀察者都通知到會花費不少時間。 因此在實際項目中使用的話,你們須要注意以上的問題。