最近学习vue源码,在学习关于数据双向绑定的时候。看了好几遍,还是有很多点不太理解。部门的大神建议自己按照思想模仿的写一个,体会会深很多。于是照做了,还真是神清气爽。这篇文章记录自己在写demo时遇见的思路和问题,日后回忆复习起来也方便。
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,因为数据的属性值还有可能是对象
Dep(Dependency):顾名思义,是Observer和Watcher的连接。如何连接呢?每一个observer会创建一个Dep实例,实例在get数据的时候为数据收集watcher,在set的时候执行watcher内的回调方法。
以上是vue中的做法。我自己实现demo的时候就是根据这个思路进行实现的。
先是Observer,递归将属性设置为访问器属性,代码如下:
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的必要了。