原生js实现检测对象变化

首先,js中的属性分为俩种,一种是数据属性,一种是访问器属性。

    var data = {};
    data.name = '田二黑';

上面这种就是数据属性。当然和下面效果一样:

    Object.defineProperty(obj, 'name', {  
        value: '田二黑',       // 属性的值  
        writable: true,     // 是否可写  
        enumerable: true,   // 是否能够通过for in 枚举  
        configurable: true  // 是否可使用 delete删除  
    })

当然我们可以定义访问器属性 get  set,当你读取age属性时,会自动调用get,设置属性时会调用set

    Object.defineProperty(obj, 'age', {  
        get: function(){  
            return 20;  
        },  
        set: function(newVal){  
            this.age += 20;  
        }  
    })

其中,vue就是利用访问器实现的数据双向绑定,像下面这个例子(可能你家没满月的孩子都会写了)

    new Vue({  
       data:{  
          name:'田二黑',  
            age:21  
        }  
    })

如果我们把data对象的属性全部转化为访问器属性,那我们不就可以检测变化了,修改时候会调用set访问器,在里面回调通知不就行了?

    const OP = Object.prototype;  
    const types = {  
      obj:'[object Object]',  
      array:'[object Array]'  
    }  
    export default class Jsonob{  
        constructor(obj,cb){  
            if(OP.toString.call(obj) !== types.obj){  
                console.log('请传入一个对象');  
                return false;  
            }  
            this._callback = cb;  
            this.observe(obj);  
        }  
        observe(obj){  
            Object.keys(obj).forEach((key)=>{  
                let val = obj[key];  
                Object.defineProperty(obj,key,{  
                    get:function(){  
                        return val;  
                    },  
                    set:(function(newVal){  
                        this._callback(newVal)  
                        val = newVal  
                    }).bind(this)  
                })  
            },this)  
        }  
    }  

上面代码声明了类Jsonob,接收要监听的对象和回调函数;observe方法,遍历该对象,并依次将对象属性转为访问器属性,在set中回调通知。

接下来我们测试一下

    import Jsonob from './jsonOb'  
    var data = {  
        a: 200,  
        level1: {  
            b: 'str',  
            c: [1, 2, 3],  
            level2: {  
                d: 90  
            }  
        }  
    }  
    var cb = (val)=>{  
        console.log(val)  
    }  
    new Jsonob(data,cb);  
    data.level1.level2.d = 50  

当修改对象data中属性时,回调打印出新的值。这样还没结束,我的旧值去哪了,我想获取旧值咋办?并且如果我设置的新值又是个对象咋办

    let val = obj[key];  
    Object.defineProperty(obj,key,{  
    get:function(){  
         return val;  
        },  
    set:(function(newVal){  
         this._callback(newVal)  
         val = newVal  
        }).bind(this)  
    })

上面的val = obj[key];存储的不就是旧值吗?于是修改代码如下

    Object.keys(obj).forEach((key)=>{  
                let oldVal = obj[key];  
                Object.defineProperty(obj,key,{  
                    get:function(){  
                        return oldVal;  
                    },  
                    set:(function(newVal){  
                        if(oldVal !== newVal){  
                            if(OP.toString.call(newVal) === '[object Object]'){  
                                    this.observe(newVal);  
                                }  
                            this._callback(newVal,oldVal)  
                            oldVal = newVal  
                        }  
                    }).bind(this)  
                })  
                if(OP.toString.call(obj[key]) === types.obj){  
                    this.observe(obj[key])  
                }  
            },this)

判断修改的值是否为对象,如果是对象,则继续转换新增的值的属性为访问器属性。在回调中就能接收新值和旧值。当然相信你已经发现了,

data.leavel.c是个数组,当我们push,shift等操作时还监听不到,首先,当我们调用数组的push等方法时,是执行的数组原型上的方法,那我们重

写原型上的这些方法,在这些方法里面监听不就ok了,像这样

    Array.prototype.push = function(){  
        /********/  
    Array.prototype.shift= function(){  
        /********/  
    }


数组有push,shift,pop,unshift等等,你要重写那么多方法并实现其功能,就算你实现了,并且不影响其他代码中数组的使用,性能上来说也是不

能相提并论的。那我们怎么实现?我们可不可以让数组实例的原型指向一个我们自定义的对象fakeprototype,当我们调用push方法时,调用的是该

对象上的push方法,在方法里面监听变化,然后在调用Array.prototype真正原型对象上的push方法不就行了。代码实现如下:

    const OP = Object.prototype;  
    const types = {  
      obj:'[object Object]',  
      array:'[object Array]'  
    }  
    const OAM =['push','pop','shift','unshift','short','reverse','splice']  
    export default class Jsonob{  
        constructor(obj,cb){  
            if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){  
                console.log('请传入一个对象或数组');  
                return false;  
            }  
            this._callback = cb;  
            this.observe(obj);  
        }  
        observe(obj){  
            if(OP.toString.call(obj) === types.array){  
                this.overrideArrayProto(obj);  
            }  
            Object.keys(obj).forEach((key)=>{  
                let oldVal = obj[key];  
                Object.defineProperty(obj,key,{  
                    get:function(){  
                        return oldVal;  
                    },  
                    set:(function(newVal){  
                        if(oldVal !== newVal){  
                            if(OP.toString.call(newVal) === '[object Object]'){  
                                this.observe(newVal);  
                            }  
                            this._callback(newVal,oldVal)  
                            oldVal = newVal  
                        }  
                    }).bind(this)  
                })  
                if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array){  
                    this.observe(obj[key])  
                }  
            },this)  
        }  
        overrideArrayProto(array){  
                // 保存原始 Array 原型  
            var originalProto = Array.prototype,  
                // 通过 Object.create 方法创建一个对象,该对象的原型是Array.prototype  
                overrideProto = Object.create(Array.prototype),  
                self = this,  
                result;  
                // 遍历要重写的数组方法  
                OAM.forEach((method)=>{  
                    Object.defineProperty(overrideProto,method,{  
                        value:function(){  
                            var oldVal = this.slice();  
                            //调用原始原型上的方法  
                            result = originalProto[method].apply(this,arguments);  
                            //继续监听新数组  
                            // self.observe(this);  
                            self._callback(this,oldVal);  
                            return result;  
                        }  
                    })  
                });  
            // 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto  
            array.__proto__ = overrideProto;  
              
        }  
    }

当我们再去对data.leave1.c.push()的时候,就能监听到变化。然而还没有完,我们现在只是知道了修改的新值和旧值,我们修改的哪个属性啊?我们

现在的程序还无法知道,像vue,在模板中

{{name}}
{{age}}
如果name变化,只是修改第一个div,这就是知道修改哪个属

性的好像,不然只能对模板重新全部刷新,性能肯定是不如局部修改的。因此我们还要在代码的基础上加个路径变量,表示是data的哪个属性。

    const OP = Object.prototype;  
    const types = {  
      obj:'[object Object]',  
      array:'[object Array]'  
    }  
    const OAM =['push','pop','shift','unshift','short','reverse','splice']  
    export default class Jsonob{  
        constructor(obj,cb){  
            if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){  
                console.log('请传入一个对象或数组');  
                return false;  
            }  
            this._callback = cb;  
            this.observe(obj);  
        }  
        observe(obj,path){  
            if(OP.toString.call(obj) === types.array){  
                this.overrideArrayProto(obj,path);  
            }  
            Object.keys(obj).forEach((key)=>{  
                let oldVal = obj[key];  
                let pathArray = path&&path.slice();  
                if(pathArray){  
                    pathArray.push(key);  
                }  
                else{  
                    pathArray = [key];  
                }  
                Object.defineProperty(obj,key,{  
                    get:function(){  
                        return oldVal;  
                    },  
                    set:(function(newVal){  
                        if(oldVal !== newVal){  
                            if(OP.toString.call(newVal) === '[object Object]'){  
                                this.observe(newVal,pathArray);  
                            }  
                            this._callback(newVal,oldVal,pathArray)  
                            oldVal = newVal  
                        }  
                    }).bind(this)  
                })  
                if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array){  
                    this.observe(obj[key],pathArray)  
                }  
            },this)  
        }  
        overrideArrayProto(array,path){  
                // 保存原始 Array 原型  
            var originalProto = Array.prototype,  
                // 通过 Object.create 方法创建一个对象,该对象的原型是Array.prototype  
                overrideProto = Object.create(Array.prototype),  
                self = this,  
                result;  
                // 遍历要重写的数组方法  
                OAM.forEach((method)=>{  
                    Object.defineProperty(overrideProto,method,{  
                        value:function(){  
                            var oldVal = this.slice();  
                            //调用原始原型上的方法  
                            result = originalProto[method].apply(this,arguments);  
                            //继续监听新数组  
                            self.observe(this,path);  
                            self._callback(this,oldVal,path);  
                            return result;  
                        }  
                    })  
                });  
            // 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto  
            array.__proto__ = overrideProto;  
              
        }  

当第一次调用observe时,path为空,则pathArray将当前key传入,如果不为空,则继续追加path。好了,我们现在的程序算是比较完整了,知道

要修改的属性,新值和就旧值。

你可能感兴趣的:(原生js实现检测对象变化,web前端)