1. 程式人生 > >用proxy寫一個遞迴來監聽巢狀物件甚至JSON

用proxy寫一個遞迴來監聽巢狀物件甚至JSON

其實那個監聽方式有個大bug,當不停的update 監聽物件以後就會導致記憶體洩漏。因為每次在呼叫get的時候都會生成一個新的proxy,後來才知道是多傻。

經過我一天時間的思考,用了一個遞迴的方法去解決巢狀物件的監聽問題。

下面是程式碼:

//傳遞兩個引數,一個是object, 一個是proxy的handler
//如果是不是巢狀的object,直接加上proxy返回,如果是巢狀的object,那麼進入addSubProxy進行遞迴。 
function toDeepProxy(object, handler) {
    if (!isPureObject(object)) addSubProxy(object
, handler); return new Proxy(object, handler); //這是一個遞迴函式,目的是遍歷object的所有屬性,如果不是pure object,那麼就繼續遍歷object的屬性的屬性,如果是pure object那麼就加上proxy function addSubProxy(object, handler) { for (let prop in object) { if ( typeof object[prop] == 'object') { if (!isPureObject(object
[prop])) addSubProxy(object[prop], handler); object[prop] = new Proxy(object[prop], handler); } } object = new Proxy(object, handler) } //是不是一個pure object,意思就是object裡面沒有再巢狀object了 function isPureObject(object) { if (typeof object!== 'object'
) { return false; } else { for (let prop in object) { if (typeof object[prop] == 'object') { return false; } } } return true; } }

這個函式的關鍵點就在於,你是從巢狀物件的第一個屬性進行判斷加proxy還是從巢狀物件的最末端先進行判斷加proxy, 此函式是後者。

前者我也除錯過,行不通,就不贅述了。而且前者向後遞迴有個問題,在初始化的時候會觸發不必要的get/set 指令。

一個有趣的事情是,在js中array也是object, 所以這個方法也是可以監聽array的, 衍生開去,這個方法可以監聽整個JSON資料結構。

但是這個函式有個缺點,就是會直接汙染 需要監聽的target, 如果你不想target被汙染,那麼這個方法就不可行了。

下面是用法:

//這是一個嵌套了物件和陣列的物件
let object = {
    name: {
        first: {
            four: 5,
            second: {
                third: 'ssss'
            }
        }
    },
    class: 5,
    arr: [1, 2, {arr1:10}],
    age: {
        age1: 10
    }
}
//這是一個嵌套了物件和陣列的陣列
let objectArr = [{name:{first:'ss'}, arr1:[1,2]}, 2, 3, 4, 5, 6]

//這是proxy的handler
let handler = {
    get(target, property) {
        console.log('get:' + property)
        return Reflect.get(target, property);
    },
    set(target, property, value) {
        console.log('set:' + property + '=' + value);
        return Reflect.set(target, property, value);
    }
}
//變成監聽物件
object = toDeepProxy(object, handler);
objectArr = toDeepProxy(objectArr, handler);

//進行一系列操作
console.time('pro')
objectArr.length
objectArr[3];
objectArr[2]=10
objectArr[0].name.first = 'ss'
objectArr[0].arr1[0]
object.name.first.second.third = 'yyyyy'
object.class = 6;
object.name.first.four
object.arr[2].arr1
object.age.age1 = 20;
console.timeEnd('pro')

下面是結果:
這裡寫圖片描述

可以看到,每次操作,不管物件,巢狀物件,陣列,巢狀陣列都有被呼叫到,而且在3ms左右完成,沒有記憶體洩漏的問題。

接下來封裝一下,用兩種方法封裝,一種是類,一種是函式

類:

class Watch {
    constructor(obj) {
        this.emit = dispatchEvent.bind(document);
        this.listen = addEventListener.bind(document);
        let customEventData = { detail: obj };
        this.eventUpdated = new CustomEvent('updated', customEventData);
        this.eventRead = new CustomEvent('read', customEventData);
        this.eventChanged = new CustomEvent('changed', customEventData);
        this.targetObj = obj;
    }

    createProxy() {
        let _this = this;
        let handler = {
            get(target, property) {
                _this.emit(_this.eventRead);
                return Reflect.get(target, property);
            },
            set(target, property, value) {
                _this.emit(_this.eventUpdated);
                if (target[property] != value) _this.emit(_this.eventChanged);
                return Reflect.set(target, property, value);
            }
        }

        return toDeepProxy(_this.targetObj, handler);


        function toDeepProxy(object, handler) {
            if (!isPureObject(object)) addSubProxy(object, handler);
            return new Proxy(object, handler);

            function addSubProxy(object, handler) {
                for (let prop in object) {
                    if (typeof object[prop] == 'object') {
                        if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
                        object[prop] = new Proxy(object[prop], handler);
                    }
                }
                object = new Proxy(object, handler)
            }

            function isPureObject(object) {
                if (typeof object !== 'object') {
                    return false;
                } else {
                    for (let prop in object) {
                        if (typeof object[prop] == 'object') {
                            return false;
                        }
                    }
                }
                return true;
            }
        }

    }

    on(eventStr, callback) {
        let _this = this;
        if (!/,+/.test(eventStr)) {
            this.listen(eventStr, (e) => {
                if (e.detail == _this.targetObj) callback();
            });
        } else {
            let eventStrArr = eventStr.split(',');
            for (let i = 0, len = eventStrArr.length; i < len; i++) {
                this.listen(eventStrArr[i].trim(), (e) => {
                    if (e.detail == _this.targetObj) callback();
                    callback()
                });
            }
        }
    }
}

用法:

let obj1 = {
    name: 'ss',
    age: 10
}

let obj2 = {
    class: 'ff'
}

let watcher = new Watch(obj1);
let watcher2 = new Watch(obj2);

obj1 = watcher.createProxy();
obj2 = watcher2.createProxy();

watcher.on('read, updated', function () {
    console.log(obj1)
});

watcher2.on('read, updated', function () {
    console.log(obj2)
});

obj1.name
obj2.class

這是用了addEventListener來監聽物件的變化,但是問題是要呼叫全域性變數document

下面用函式的方法:

function watchOut(obj, opts) {
    let handler = {
        get(target, property) {
            opts.beforeRead(target, property);
            let result = Reflect.get(target, property);
            opts.read(target, property); 
            return result; 
        },
        set(target, property, value) {
            opts.beforeUpdated(value, property, value);
            if(target[property] != value) opts.beforeChanged(value, property, value);
            let result = Reflect.set(target, property, value);
            opts.updated(value, property, value);
            opts.changed(value, property, value);
            return result; 
        }
    }
    return toDeepProxy(obj, handler);

    function toDeepProxy(object, handler) {
        if (!isPureObject(object)) addSubProxy(object, handler);
        return new Proxy(object, handler);

        function addSubProxy(object, handler) {
            for (let prop in object) {
                if (typeof object[prop] == 'object') {
                    if (!isPureObject(object[prop])) addSubProxy(object[prop], handler);
                    object[prop] = new Proxy(object[prop], handler);
                }
            }
            object = new Proxy(object, handler)
        }

        function isPureObject(object) {
            if (typeof object !== 'object') {
                return false;
            } else {
                for (let prop in object) {
                    if (typeof object[prop] == 'object') {
                        return false;
                    }
                }
            }
            return true;
        }
    }
}

呼叫方法:

    let obj3 = watchOut(
        { name: { second: '99' } },
        {
            beforeRead(target,property){
                console.log(target, property, 'beforeRead'); 
            },
            read(target, property) {
                console.log('afterRead');
            },
            beforeUpdated(target, property, value) {

                console.log('beforeUpdated');
            },
            beforeChanged(target, property, value) {
                console.log('beforeChanged')
            },
            updated(target, property, value) {
                console.log('updated');
            },
            changed(target, property, value) {
                console.log('changed')
            }

        });

obj3.name.second

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
經過我的測試發現,如果用上面的方法對一個巢狀對像進行toDeepProxy以後,如果對此proxy進行賦值,而且這個值是個物件時,那麼還是不能對賦的新值進行監聽,因為我們沒有在handler的set裡面對新值進行代理。
所以我們改下handler。

 let handler = {
        get(target, property) {
            opts.beforeRead(target, property);
            let result = Reflect.get(target, property);
            opts.read(target, property);
            return result;
        },
        set(target, property, value) {
            opts.beforeUpdated(value, property, value);
            if (target[property] != value) opts.beforeChanged(value, property, value);
            let result = Reflect.set(target, property, value);
            opts.updated(value, property, value);
            opts.changed(value, property, value);
            if(typeof value == 'object') {
                target[property] = toDeepProxy(target[property], handler); //加了這麼一句,當value為一個物件時,對此物件也進行深度代理
            }
            return result;
        }
    }