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;i
Watcher
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;i
Document