最近学习vue源码,在学习关于数据双向绑定的时候。看了好几遍,仍是有不少点不太理解。部门的大神建议本身按照思想模仿的写一个,体会会深不少。因而照作了,还真是神清气爽。这篇文章记录本身在写demo时碰见的思路和问题,往后回忆复习起来也方便。vue
vue的双向数据绑定是区分普通对象和数组的。数组的比较复杂,下篇再介绍。今天介绍vue对于对象数据的双向绑定。vue是经过数据劫持的方式来实现双向数据绑定的。数据劫持的核心就是object.defineProperty().简单介绍下这个方法。这个方法是es5定义的,通过该方法定义的对象属性会变成访问器属性。如下是一个简单的例子:数组
function Observer(obj,key,value){
if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ }, set:function(){ } }) }
访问器属性的最大特色即是内部能够指定get、set方法。在对属性进行值访问的时候会调用定义的get方法,对属性进行赋值的时候会调用set方法。数据结构
接着说双向数据绑定。双向数据绑定分为如下三个部分:函数
Observer:负责数据劫持,把全部的属性转换成访问器属性,达到对数据进行观测的目的。须要对数据进行递归观测,由于数据的属性值还有多是对象学习
Watcher:数据的观察者,在数据发生变化以后执行的相应的回调函数,须要对数据进行递归watch,由于数据的属性值还有多是对象this
Dep(Dependency):顾名思义,是Observer和Watcher的链接。如何链接呢?每个observer会建立一个Dep实例,实例在get数据的时候为数据收集watcher,在set的时候执行watcher内的回调方法。es5
以上是vue中的作法。我本身实现demo的时候就是根据这个思路进行实现的。spa
先是Observer,递归将属性设置为访问器属性,代码以下:prototype
function Observer(obj,key,value){
if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ new arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ return value; }, set:function(newVal){ if(value===newVal)return;
value = newVal; } }) }
先判断属性值value是否是对象,若是是,还须要对对象进行递归调用,观测数据双向绑定
接着是Watcher,目的在于在数据发生变化的时候执行相应的回调函数
function Watcher(data,k,v,fn){ if(Object.prototype.toString.call(data)==='[object Object]'){ Object.keys(v).forEach(function(key){ new arguments.callee(v,key,v[key],fn); }) } this.fn = fn; data[k]; }
也是先判断是否是对象,是的话递归调用观察属性值。如何让watcher和observer产生联系呢?
observer中对全部的属性设置成了访问器属性,因此若是咱们在watcher中调用属性,求属性的值就会调用到属性的get方法。
既然observer和watcher能够在get方法内产生链接,那么是否是能够在get的时候收集不一样的watcher,而后在set函数呗调用的时候执行这些watcher中的方法。这样就须要在observer中引入一个对象,在get函数内收集watcher,在set函数内遍历执行watcher的回调方法,已达到动态响应的目的。
这个对象就是Dep。每一个observer内都会实例化一个Dep对象,用于收集watcher和用于执行watcher,根据这个思路,能够获得如下的代码:
function Dep(){ var sub=[]; this.addSub=function(watcher){ this.sub.push(watcher); }; this.notify=function(){ this.sub.forEach(function(watcher){ watcher.fn(); }) } }
根据思路,Dep当中须要一个存储watcher的数据结构,从添加和遍历的角度选择,数组比较合适。而后是须要一个添加watcher的方法,在就是须要一个遍历watcher的方法。进而,得出了以上的代码。
如今三个组件都已经有了,那他们之间怎么协调工做呢?按照以前的思路,和咱们如今有的代码。在observer中加入Dep收集和执行依赖的代码。就发现,存在怎么在get中获得watcher的实例的问题。既然Dep自己做为watcher和observer的链接桥梁,那这个事情就让Dep作吧。此时须要作的事情是,须要一个变量,在Watcher中收集watcher实例,在get中将Watcher实例放入Dep实例的数组中,以便于set中使用。
思考这个变量的功能,他不能出如今构造函数和原型链中,这样watcher的变化会实时的提如今每一个实例上。那么只有在构造函数自己这个函数对象定义这个变量比较合适了。函数对象上的变量不会经过new操做符影响到全部实例,又能完成存储watcher的功能。
先在watcher中收集,那么watcher中的代码以下:
function Watcher(data,k,v,fn){ if(Object.prototype.toString.call(data)==='[object Object]'){ Object.keys(v).forEach(function(key){ new arguments.callee(v,key,v[key],fn); }) } this.fn = fn; Dep.target = this;// data[k]; Dep.target=null;// }
在watcher调用属性求值以前,将watcher保存到Dep.target变量中,在求值以后(get中Dep实例收集了以后)将该值置为null,这样就不会在别的属性求值的时候影响到别的属性。
已经在Watcher中收集到了watcher实例,那么observer中如何使用呢。看以下代码:
function Observer(obj,key,value){ var dep = new Dep(); if(Object.prototype.toString.call(value)=='[object Object]'){ Object.keys(value).forEach(function(key){ new arguments.callee(value,key,value[key]); }) } Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get:function(){ if(Dep.target){//存储依赖 dep.addSub(Dep.target);// }// return value; }, set:function(newVal){ if(value===newVal)return; value = newVal; dep.notify();//执行依赖 } }) }
到这一步,咱们基本上对象的双向绑定已经完成了。全部的功能都已经实现了。
在我本身运行调试的时候发现一个问题。若是set的值又是一个对象,那么对象的属性改变将没法获得监控。因此,在set中加上以上代码就完整了:
if(Object.prototype.toString.call(value) ==='[object Object]'){ Object.keys(value).forEach(function(key){ new Observer(value,key,value[key]); new Watcher(value,key,value[key],function(v,key){ console.log('你修改了数据'); // document.getElementById('dd').innerHTML=v.key; }); })
}
以上就是我根据vue实例实现的关于对象的实时监听的小demo了。
最后,我想了想。为何vue要分这三个部分作呢?细想起来watcher主要的功能也就是set的时候的回调函数。若是没学习过vue的源码,应该就是直接把函数传入observer作回调函数就是了。这样watcher省了,dep也不用了。
可是又一想,同一个属性可能同时有几个watcher,就会要执行多个回调函数。那么就有了watcher和dep的必要了。