Vue监听数据对象变化源码
监听数据对象变化,最容易想到的是建立一个需要监视对象的表,定时扫描其值,有变化,则执行相应操作,不过这种实现方式,性能是个问题,如果需要监视的数据量大的话,每扫描一次全部的对象,需要的时间很长。当然,有些框架是采用的这种方式,不过他们用非常巧妙的算法提升性能,这不在我们的讨论范围之类。
Vue中数据对象的监视,是通过设置ES5的新特性(ES7都快出来了,ES5的东西倒也真称不得新)Object.defineProperty()中的set、get来实现的。
目标
与官方文档第一个例子相似,不过也有简化,因为这篇只是介绍下数据对象的监听,不涉及文本解析,所以文本解析相关的直接舍弃了:
浏览器显示:
HelloVue!
在控制台输入诸如:
app.message='Changed!'
之类的命令,浏览器显示内容会跟着修改。
Object.defineProperty
引用MDN上的定义:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。
与此相生相伴的还有一个Object.getOwnPropertyDescriptor():
Object.getOwnPropertyDescriptor()返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
下面的例子用一种比较简单、直观的方式来设置setter、getter:
vardep=[]; functiondefineReactive(obj,key,val){ //有自定义的property,则用自定义的property varproperty=Object.getOwnPropertyDescriptor(obj,key); if(property&&property.configurable===false){ return; } vargetter=property&&property.get; varsetter=property&&property.set; Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ varvalue=getter?getter.call(obj):val; dep.push(value); returnvalue; }, set:function(newVal){ varvalue=getter?getter.call(obj):val; //set值与原值相同,则不更新 if(newVal===value){ return; } if(setter){ setter.call(obj,newVal); }else{ val=newVal; } console.log(dep); } }); }
vara={}; defineReactive(a,'a',12); //调用getter,12被压入dep,此时dep值为[12] a.a; //调用setter,输出dep([12]) a.a=24; //调用getter,24被压入dep,此时dep值为[12,24] a.a;
Observer
简单说过Object.defineProperty之后,就要开始扯Observer了。observer,中文解释为“观察者”,观察什么东西呢?观察对象属性值的变化。故此,所谓observer,就是给对象的所有属性加上getter、setter,如果对象的属性还有属性,比如说{a:{a:{a:'a'}}},则通过递归给其属性的属性也加上getter、setter:
functionObserver(value){ this.value=value; this.walk(value); } Observer.prototype.walk=function(obj){ varkeys=Object.keys(obj); for(vari=0;iWatcher
Observer通过设置数据对象的getter、setter来达到监听数据变化的目的。数据被获取,被设置、被修改,都能监听到,且能做出相应的动作。
现在还有一个问题就是,谁让你监听的?
这个发出指令的就是Watcher,只有Watcher获取数据才触发相应的操作;同样,修改数据时,也只执行Watcher相关操作。
那如何讲Observer、Watcher两者关联起来呢?全局变量!这个全局变量,只有Watcher才做修改,Observer只是读取判断,根据这个全局变量的值不同而判断是否Watcher对数据进行读取,这个全局变量可以附加在dep上:
dep.target=null;
根据以上所述,简单整理下,代码如下:
functionWatcher(data,exp,cb){ this.data=data; this.exp=exp; this.cb=cb; this.value=this.get(); } Watcher.prototype.get=function(){ //给dep.target置值,告诉Observer这是Watcher调用的getter dep.target=this; //调用getter,触发相应响应 varvalue=this.data[this.exp]; //dep.target还原 dep.target=null; returnvalue; }; Watcher.prototype.update=function(){ this.cb(); }; functionObserver(value){ this.value=value; this.walk(value); } Observer.prototype.walk=function(obj){ varkeys=Object.keys(obj); for(vari=0;i vardata={a:1}; newObserver(data); newWatcher(data,'a',function(){console.log('itworks')}); data.a=12; data.a=14;上面基本实现了数据的监听,bug肯定有不少,不过只是一个粗糙的demo,只是想展示一个大概的流程,没有扣到非常细致。
Dep
上面几个例子,dep是个全局的数组,但凡new一个Watcher,dep中就要多一个Watcher实例,这时候不管哪个data更新,所有的Watcher实例的update都会执行,这是不可接受的。
Dep抽象出来,单独搞一个构造函数,不放在全局,就能解决了:
functionDep(){ this.subs=[]; } Dep.prototype.addSub=function(sub){ this.subs.push(sub); }; Dep.prototype.notify=function(){ varsubs=this.subs.slice(); for(vari=0;i利用Dep将上面的代码改写下就好了(当然,此处的Dep代码也不完全,只是一个大概的意思罢了)。
Vue实例代理data对象
官方文档中有这么一句话:
每个Vue实例都会代理其data对象里所有的属性。
vardata={a:1}; varvm=newVue({data:data}); vm.a===data.a//->true //设置属性也会影响到原始数据 vm.a=2 data.a//->2 //...反之亦然 data.a=3 vm.a//->3这种代理看起来很麻烦,其实也是可以通过Object.defineProperty来实现的:
functionVue(options){ vardata=this.data=options.data; varkeys=Object.keys(data); vari=keys.length; while(i--){ proxy(this,keys[i]; } } functionproxy(vm,key){ Object.defineProperty(vm,key,{ configurable:true, enumerable:true, //直接获取vm.data[key]的值 get:function(){ returnvm.data[key]; }, //设置值的时候直接设置vm.data[key]的值 set:function(val){ vm.data[key]=val; } }; }捏出一个Vue,实现最初目标
varVue=(function(){ varWatcher=functionWatcher(vm,exp,cb){ this.vm=vm; this.exp=exp; this.cb=cb; this.value=this.get(); }; Watcher.prototype.get=functionget(){ Dep.target=this; varvalue=this.vm._data[this.exp]; Dep.target=null; returnvalue; }; Watcher.prototype.addDep=functionaddDep(dep){ dep.addSub(this); }; Watcher.prototype.update=functionupdate(){ this.run(); }; Watcher.prototype.run=functionrun(){ this.cb.call(this.vm); } varDep=functionDep(){ this.subs=[]; }; Dep.prototype.addSub=functionaddSub(sub){ this.subs.push(sub); }; Dep.prototype.depend=functiondepend(){ if(Dep.target){ Dep.target.addDep(this); } }; Dep.prototype.notify=functionnotify(){ varsubs=this.subs.slice(); for(vari=0;iDocument