不久之前我才剛在上一篇文章中給 Vue-On-Clickout 加入了 Vue 3 的支援,但是現在立刻又要再跨出一大步了。最近幾天裡面,我新完成了 Clickout-Event 這個套件,它具有 Vue-On-Clickout 的一切功能,而且在強大程度上遠超過後者——首先,它還提供了更多的事件:除了 clickout
以外,包括 dblclick
, mousedown
, mouseup
, touchstart
, touchend
都有對應的「out」事件可以用;但再來更重要的是,它支援任何的前端框架,甚至無框架的 vanilla JavaScript 和 HTML 屬性也都通。
之所以 Clickout-Event 能夠做到這種程度,是因為它用了跟 Vue-On-Clickout 截然不同的底層原理。上一篇文章中我解釋過,這種偵測滑鼠外部點擊的套件必然都有兩個部份,一個是管理事件觸發和傳遞本身,另一個則是管理受關注的元件。前者的部份在 Clickout-Event 中採用的仍然是幾乎一樣的機制,主要只是差在支援更多種事件、以及在 stopPropagation()
的部份稍微改良機制而已,所以這部份不用多解說,重點仍然是在後者。
在上一篇文章中,關於這部份我是這麼說的:
在自訂 directive 當中,可以透過 bind 和 unbind 的 hook……自訂在元件加入或移出 DOM 的時候要做什麼事情。這部份其實確實是自訂 directive 很方便的一個地方,老實說用別的方法來追蹤元件的出入 DOM 都沒有這個作法來得好。
說來好笑,這部份真的是因為我先前太孤陋寡聞才會這麼覺得。我自從改行以來,顯然對於過去十年間新的網頁 API 還沒有完全趕上時代進度,結果一直到幾天前我才偶然知道原來現在有 MutationObserver
這個東西;當時我一看到它,我就知道它就是我一直在尋找的解答,因為它的能力就是觀測一切頁面上的元件變動,不管頁面上用的是什麼前端框架來動態產生元件、或是瀏覽器自己的頁面渲染過程都逃不過它的監控,所以只要這個用上去,任何自訂的機制都可以輕易地和任何前端框架完美整合。而且這個東西其實一點也不新,甚至連 IE 11 都有(況且現在 IE 也已經正式壽終正寢了),所以基本上也不太需要擔心什麼 CanIUse 的問題。
所以底下我們就來快速複習一下 MutationObserver
的用法。粗略來說,它可以觀測三種類型的頁面變動:元件的增減、元件屬性的改變、以及文字內容的變化。這邊我們感興趣的應用只有第一個類型。首先我們需要宣告一個實體:
let observer = new MutationObserver(list => {
for(let record of list) {
for(let node of record.addedNodes) {
...
}
for(let node of record.removedNodes) {
...
}
}
});
MutationObserver
的建構式傳入的 callback 接受一個 MutationRecord[]
型態的參數,這個 callback 會在瀏覽器完成一整個批次的頁面變動之後才被呼叫(以提昇效能)。通常針對每一個 MutationRecord
我們還需要去根據其 type
屬性來判斷它是對應哪一種頁面變動,但是反正我們待會只會監視「元件增減」這一種類型的變動,所以這個判別我就省略了。
而 MutationRecord
介面的 addedNodes
與 removedNodes
屬性都是 NodeList
型態、分別列舉了單次頁面變動中被加入以及被移除的 Node
。不過需要注意!基本上這裡面列出的只會是被加入或移除的結點樹中最上層的那一個 Node
,而不是全部的 Node,所以如果我們需要去解析整個結點樹,那我們就還必須記得額外去遞迴遍歷每一個 Node
的 childNodes
(細節可參考 Clickout-Event 的原始碼)。
有了實體之後,再來就是啟動觀測:
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
其中的參數表示我們要對 document.documentElement
的內容進行觀測(而非 document.body
,因為假如這個 script 是被放在 <head>
裡面的話,body
就還不存在),觀測的類別是元件增減、而且要包含所有子節點樹(否則就只有 documentElement
的直屬子節點變化會被回報)。
有了 MutationObserver
的觀測能力之後,Clickout-Event 的所作的事情就是在有任何元件加入 DOM 的時候,遞迴去檢查每一個加入的元件:
- 它是否曾經被註冊過任何的 out 事件?(我透過修改
addEventListener
方法來記錄這件事) - 它是否具有任何例如
onclickout
這樣的 HTML 屬性?
如果其中一項有的話,就把它加入關注清單中。而當有元件被移出 DOM 的時候,則檢查當前的關注清單中是否有任何一個屬於被移除的元件,有的話就從關注清單中移除即可。
MutationObserver
是真的非常強的一個東西,在工作上它也幫我解決掉了好幾個之前一直很頭大的、第三方套件與 Vue.js 之間的相容問題,非常推薦大家也玩看看。
留言