如何在 Vue 觀察 DOM: MutationObserver

我腦袋枯竭,不想寫文章……最近怎麼了?

問題

總之,當你想觀察 Vue 內部的 DOM 屬性時,你可能會這麼寫:

<template>
    <main>
        <nav ref="foo">
            <!-- DOMs -->
        </nav>
    </main>
</template>

<script>
export default {
    watch: {
        "$refs.foo"(newval) {
            // Do something...
        },
    },
}
</script>

**這麼做會失敗。**因為 Vue 的響應式原理是 Object.defineProperty,沒有實做到 DOM 的層面。

我覺得我有點北七。

解答

那麼該怎麼做?答案是使用MutationObserver──這個 API 可以追蹤特定的 DOM 更動,並提供該 DOM 具體的資訊。

你需要丟幾個東西給 MutationObserver

  1. 目標 DOM。
  2. 監聽時的動作。
  3. 設定。

所以我們試試看這樣:

<template>
    <main>
        <nav ref="foo">
            <img src="https://example.com/bar.png" />
            <div ref="baz">{{ text }}</div>
        </nav>
    </main>
</template>

<script>
export default {
    data() {
        return { observer: null, text: "" };
    },
    methods: {
        change_it() { this.flag = !this.flag; },
        active_observer() {
            const callback = (mutationsList, observer) => {
                // 在這裡,mutationsList 會傳一個被變動的 DOM 狀態陣列
                // 而 observer 則回傳被呼叫的 MutationObserver 本身
                // 詳細參見: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver
            };
            const dom = this.$refs.foo;
            const cfg = { attributes: true, childList: true, subtree: true };
            this.observer = new MutationObserver( callback );
            this.observer.observe( dom, cfg );
        }
    },
    mounted() {
        this.active_observer();
    },
    beforeDestroy() {
        if( this.observer ) {
            this.observer.disconnect();
        }
    },
};
</script>

記住,觀察 DOM 只能放在 mounted 裡面。

這樣做以後,這個組件內的 DOM 變化,被綁定的 observer 狀態,就可以告訴我們,有什麼 DOM 變動了:像是可以用 getBoundingClientRect API 來監測 DOM 的位置。

無法解決的問題

這樣就大功告成了,但有一些問題……

首先,如果某個 DOM 本身尚未存在,那個 DOM 將只會是 null,而 MutationObserver 愛莫能助。

所以如果把範例的 this.active_observer(); 放在剛建立組件的 created 而非已經形成 DOM 實體的 mounted 裡面,就會出現以下錯誤:

TypeError: MutationObserver.observe: Argument 1 is not an object.

所以你必須確保在開始呼叫 MutationObserver 前,該 DOM 就一定存在。


另一個問題要回頭看看範例的 HTML:

<template>
    <main>
        <nav ref="foo">
            <img ref="bar" src="https://example.com/bar.png" />
            <div ref="baz">{{ text }}</div>
        </nav>
    </main>
</template>

雖然我們可以觀察 ref="baz" 的 DOM,但如果要觀察 ref="bar" 的圖片載入、還有它對 ref="foo" 的影響,那 MutationObserver 就派不上用場了。

這樣要怎麼辦呢?

我會改天說明的。

結語

  1. 你不能用 Vue 的 watch instance 來觀察 DOM 屬性的變化,因為 Vue 響應式設計原理並沒有觀察 DOM 屬性。
  2. 所以,如果要觀察 DOM 屬性,你需要使用 MutationObserver
  3. 要使用 MutationObserver 的話,你需要確保被監聽的 DOM 自始存在;而且 MutationObserver 無法觀察圖片載入與否。