原生js实现检测对象变化

最近这段时间,前端开发开始逐渐模块化,一些MVC、MVVM等框架比较流行,比如angular、vue、react;这三个框架比较相似的有一点就是数据的双向绑定,视图的更新导致相应数据变化,数据的改变引起视图的变化。像这种魔法的操作是怎么实现的呢?像angular则是采用了‘脏值检测的方式’数据发生变更后,对于所有的数据和视图的绑定关系进行一次检测,识别是否有数据发生了改变,有变化进行处理,可能进一步引发其他数据的改变,所以这个过程可能会循环几次,一直到不再有数据变化发生后,将变更的数据发送到视图,更新页面展现。如果是手动对 ViewModel 的数据进行变更,为确保变更同步到视图,需要手动触发一次“脏值检测”。VueJS 则使用 ES5 提供的 Object.defineProperty() 方法,监控对数据的操作,从而可以自动触发数据同步。并且,由于是在不同的数据上触发同步,可以精确的将变更发送给绑定的视图,而不是对所有的数据都执行一次检测。接下来就一块实现检测对象变化的功能。(本代码使用es6编写,部分浏览器不支持,还需要使用babel进行编译)

首先,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,在模板中<div>{{name}}</div><div>{{age}}</div> 如果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。好了,我们现在的程序算是比较完整了,知道

要修改的属性,新值和就旧值。水平有限,希望指出共同进步。

个人博客:www.tmgrocery.com

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值