原生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
    评论
条件语句 v-if ,v-else,v-else-if。 后两者必须放在v-if之后 循环语句 v-for 指令需要以 site in sites 形式的特殊语法, sites 是源数据数组并且 site 是数组元素迭代的别名。 Computed VS methods 我们可以使用 methods 来替代 computed,效果上两个都是一样的,但是 computed 是基于它的依赖缓存,只有相关依赖发生改变时才会重新取值。而使用 methods ,在重新渲染的时候,函数总会重新调用执行。 事件修饰符 Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节,如:event.preventDefault() 或 event.stopPropagation()。Vue.js通过由点(.)表示的指令后缀来调用修饰符。 <!-- 阻止单击事件冒泡 --> <a stop="doThis"></a> <!-- 提交事件不再重载页面 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修饰符可以串联 --> <a prevent="doThat"></a> <!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件侦听器时使用事件捕获模式 --> <div v-on:click.capture="doThis">...</div> <!-- 只当事件在该元素本身(而不是子元素)触发时触发回调 --> <div v-on:click.self="doThat">...</div> <!-- click 事件只能点击一次,2.1.4版本新增 --> <a 按键修饰符 为最常用的按键提供了别名: <input v-on:keyup.enter="submit"> <!-- 缩写语法 --> <input @keyup.enter="submit"> Vue.js为最常用的两个指令v-bind和v-on提供了缩写方式。v-bind指令可以缩写为一个冒号,v-on指令可以缩写为@符号。 全部的按键别名: .enter .tab .delete (捕获 "删除" 和 "退格" 键) .esc .space .up .down .left .right .ctrl .alt .shift .meta 实例 组件部分不太会,em...... 钩子函数 指令定义函数提供了几个钩子函数(可选): bind: 只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。 inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。 update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新(详细的钩子函数参数见下)。 componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。 unbind: 只调用一次, 指令与元素解绑时调用。 钩子函数的参数有: el: 指令所绑定的元素,可以用来直接操作 DOM 。 binding: 一个对象,包含以下属性: name: 指令名,不包括 v- 前缀。 value: 指令的绑定值, 例如: v-my-directive="1 + 1", value 的值是 2。 oldValue: 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。 expression: 绑定值的表达式或变量名。 例如 v-my-directive="1 + 1" , expression 的值是 "1 + 1"。 arg: 传给指令的参数。例如 v-my-directive:foo, arg 的值是 "foo"。 modifiers: 一个包含修饰符的对象。 例如: v-my-directive.foo.bar, 修饰符对象 modifiers 的值是 { foo: true, bar: true }。 vnode: Vue 编译生成的虚拟节点。 oldVnode: 上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。 样式叠加测试文件:test2/addStyle.html 属性覆盖测试文件:test2/cover.html 自定义组件测试文件:test2/customComponent.html get请求测试文件:test2/get.html post请求测试文件:test2/post.html vue初探:test2/helloVue.html,test2/helloVue2.html input 和 textarea 元素中使用 v-model 实现双向数据绑定:test2/inputAndtextarea.html 两个按钮用于切换不同的列表布局:test2/layout.html 导航测试:test2/navigation.html 订单列表:test2/orderList.html 实时变更:test2/real-time-change.html 模糊搜索:test2/search.html 购物车:test2/shoppingCart.html 双向绑定:test2/two-way-binding.html 字符转换:test2/upperCase.html class属性绑定:test2/v-bind.html href 属性绑定:test2/v-test.html vue路由:test2/vueRouter.html watch监听时间:watchJianTing.html

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值