跳到主要內容

JavaScript 監聽物件屬性變更

當程式越來越長,而且又是多人開發的狀況下
一定有時候是開發者會想著 "WTF,為什麼屬性XX被改成XXX"
特別是 JavaScript 因為設計因素,本身就很容易寫髒
監聽物件屬性變化還是有時候能幫助我們找出問題的好方法

先從 DOM Object 開始,適用的處理方式是 MutationObserver 這個規格
DOM 物件中最有監聽需求的可能就是 style 屬性
打從一開始設計,JavaScript 就讓變更網頁元素的方式直接、簡單
一個 DOMObject.style.display = 'none'; 就可以讓一個元素隱藏在顯示頁面上
不過因為這方式不會經過任何 function stack
所以在碰到問題時很難知道是哪段程式呼叫了 style 的變更

處理方式示範如下:
var div = document.querySelector("...");

if (window.MutationObserver) {
    var observer = new MutationObserver(function(mutations, _self) {
        var divClosed = mutations.some(function (mutation) {
            return mutation.target.style.display === "none";
        });
        if (divClosed) {
            _self.disconnect();  // ensure to release reference
            document.body.removeChild(div);
        }
    });
    var config = { attributes: true, attributeFilter: ["style"] };
    observer.observe(div, config);
} else {
    // IE9
    div.addEventListener('DOMAttrModified', function (event) {
        if(event.target.style.display === "none") {
            document.body.removeChild(div);
        }
    }, false);
}

如果你的需求有包括 IE9 的支援,那麼 else 那段的內容會是必須的
示範程式碼會監聽目標物件的 attributes 中對 style 的變更
當目標被設定為 style.display = 'none' 隱藏後,就把他從網頁裡整個移除
監聽的物件不再被需要時,得記得呼叫 disconnect


自訂的物件也可能有這需求
良好的程式設計規劃可以在事前就很好的避免這個麻煩
雖然工程師大概都有過覺得寫 setter、getter很煩的時候
不過當出問題時,有 stacktrace 可以追蹤兇手還是很幸福的

問題出在 JavaScript 物件實在太好新增屬性了
let a = {};
a.demo = "Test";

這樣就能輕易地在物件上新增自訂屬性了,甚至可能物件的原始建立者都不知道
異於原始程式規範外的理由,可以舉一個如下:
原本只是臨時屬性,但隨著程式開發越來越大坨,拿不掉也不好追加getter、setter
或者是其他歷史因素(共業)的理由...等等
所以這個在實務上依然有可能會有額外追蹤自訂物件屬性變化的需求

這方面我們需要的是 Proxy 規格,注意 Proxy 規格並不支援 IE
Proxy 規格並不是只用在抓 bug 這方面
但是實務上最常見的應用方向可能就是抓 bug

他在 Array Object 跟 Object 使用上有些差異,先看 Array 的應用

const arrayChangeHandler = {
    set: function(target, property, value, receiver) {
        console.log(`setting ${property} with value ${value}`);
        target[property] = value;
        console.trace();
        return true;
    }
};

const originalArray = [];
const proxyToArray = new Proxy( originalArray, arrayChangeHandler );

可以參考規格中 set 以外的部分,找出最適合自己的監聽處理
在這裡我們簡單的監聽 Array 中被丟進了哪些東西,以及是誰丟進來的
我們把原始的陣列用 Proxy 包過,包過的 proxy 物件的使用方式跟原本一模一樣

當我們 proxyToArray.push(1); 的時候
因為處理函式裡的內容,我們可以在 console 看到以下訊息

setting 0 with value 1
console.trace...

setting length with value 1
console.trace...

如此我們也可窺知 JS Push Object 到 Array 的處理方式的片鱗半爪
設定數值後再設定 Array Object 的 length 屬性

接著看普通 JS Object 的處理,在Constrcutor中也能適用喔

function Demo() {
    const handler = {
   
        set(target, key, value) {
            if (key==='customValue1' || key === 'customValue2') {
                console.trace()
                console.log(`Setting value ${key} as ${value}`)
            }
   
            target[key] = value;
        },
    };
   
    const proxy = new Proxy(this, handler);
    return proxy;
}

let demo1 = new Demo();
demo1.customValue1 = "test"

console台提示的訊息為
console.trace...
Setting value customValue1 as test

透過 key,我們可以只在特定想看的屬性變化時看到訊息就好,不會被訊息掩沒
然後也可以發現,即使我們臨時有追查需求
調整程式片段來套用 Proxy 是很簡單的,當追查完畢後要復原也是如此
善用這個方便規格可以很好的協助 JS 開發者

留言