当前位置: 首页 > Web前端 > JavaScript

Vue0.11版本源码阅读系列之二:数据观察

时间:2023-03-26 21:37:02 JavaScript

上一篇介绍了创建vue实例时要做的一些事情,其中??之一就是初始化数据。本文将介绍如何进行数据观察。_initData是数据观察的起点:exports._initData=function(){//代理数据到实例vardata=this._datavarkeys=Object.keys(data)vari=keys.lengthvarkeywhile(i--){key=keys[i]if(!_.isReserved(key)){this._proxy(key)}}//观察数据Observer.create(data).addVm(this)}_proxy方法上一篇已经作为我说,就是把data数据代理给vue实例,通过this.xx可以访问到this.data.xx的数据,关键是Observer。create是Observer类的静态方法,用于为数组或对象创建一个观察对象:Observer.create=function(value){if(value&&value.hasOwnProperty('__ob__')&&value.__ob__instanceofObserver){返回值.__ob__}elseif(_.isArray(value)){returnnewObserver(value,ARRAY)}elseif(_.isPlainObject(value)&&!value._isVue){returnnewObserver(value,OBJECT)}}从这里我们可以知道vue只能观察数组和纯对象,其他的比如函数是不会观察的。主要逻辑是判断属性是否被观察到,如果是,则返回观察者对象,否则,分别对数组和对象使用不同的标志来实例化可观察对象。我们看一下Observer类:functionObserver(value,type){this.id=++uidthis.value=valuethis.deps=[]//将观察实例设置为对象或数组的一个属性,以便后面检查并使用_.define(value,'__ob__',this)if(type===ARRAY){//arraybranchvaraugment=_.hasProto?protoAugment:copyAugmentaugment(value,arrayMethods,arrayKeys)this.observeArray(value)}elseif(type===OBJECT){//objectbranchthis.walk(value)}}初始化一些属性,先看比较简单的对象分支:p.walk=function(obj){varkeys=Object.keys(obj)vari=keys.lengthvarkey,prefixwhile(i--){key=keys[i]prefix=key.charCodeAt(0)if(prefix!==0x24&&prefix!==0x5F){//跳过以$或_开头的私有属性this.convert(key,obj[key])}}}walk方法在每个上调用convert方法对象的子属性:p.convert=function(key,val){varob=this//如果属性的值也是数组或者对象,那么也需要观察,观察方法最后调用Object.createmethodvarchildOb=ob.observe(val)//每个属性都会创建一个依赖集合实例,使用闭包保存vardep=newDep()//这个属性的观察实例添加到obser属性值的vation对象if(childOb){childOb.deps.push(dep)}Object.defineProperty(ob.value,key,{enumerable:true,configurable:true,get:function(){//在这里收集依赖,Observer.target是一个全局属性,一个Thewatcherinstance,后面细说,当watcher实例在引用该属性之前赋值给这个全局属性,可以在这里引用,然后收集到该属性的dep实例列表中if(Observer.target){Observer.target.addDep(dep)}returnval},set:function(newVal){if(newVal===val)return//如果旧值是对象或者数组,必须有对应的观察实例,所以需要从对应的观察实例dep开始去掉属性varoldChildOb=val&&val.__ob__if(oldChildOb){varoldDeps=oldChildOb.depsoldDeps.splice(oldDeps.indexOf(dep),1)}val=newVal//检查newvalue,new赋值是一个对象或者数组,需要递归创建来观察实例varnewChildOb=ob.observe(newVal)if(newChildOb){newChildOb.deps.push(dep)}//通知这个属性的依赖更新dep.notify()}})}接下来看数组分支:if(type===ARRAY){varaugment=_.hasProto?protoAugment:copyAugmentaugment(value,arrayMethods,arrayKeys)this.observeArray(value)}Vue修改了数组原型上的一些方法,如:push、shift等,原因是使用这些方法操作数组不会触发属性的setter,所以Vue无法检测到更改并更新它。所以需要拦截这些方法进行修改。这里使用了两种方法。如果浏览器支持__proto__,直接通过修改数组的__proto__来设置一个新的原型对象。如果没有,则使用Object.defineProperty覆盖并添加修改后的数组方法。vararrayProto=Array.prototype//根据数组原型对象创建新对象vararrayMethods=Object.create(arrayProto);['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(method){//缓存数组的原始方法varoriginal=arrayProto[method]_.define(arrayMethods,method,functionmutator(){//这里的参数是复制一份,避免将对象直接传递给其他函数,这可能对性能不利vari=arguments.lengthvarargs=newArray(i)while(i--){args[i]=arguments[i]}//调用原方法varresult=original.apply(this,args)//获取数组的观察实例varob=this.__ob__//获取新插入的数组的值varinsertedswitch(method){case'push':inserted=argsbreakcase'unshift':inserted=argsbreakcase'splice':inserted=args.slice(2)break}//如果有新插入的值,递归观察if(inserted)ob.observeArray(inserted)//通知依赖于更新ob.notify()返回结果})})逻辑很简单,就是调用这些方法更新数组时,观察新插入的数据并通知更新,这里是调用观察对象ob的update方法方法notify:p.notify=function(){vardeps=this.depsfor(vari=0,l=deps.length;i-1)this.subs.splice(i,1)}}p.notify=function(){varsubs=_.toArray(this.subs)for(vari=0,l=subs.length;i=this.length){this.length=index+1}returnthis.splice(index,1,val)[0]})给数组原型加上$set方法,调用splice方法设置值。这个方法已经被重写了,所以可以触发更新,我们可以直接使用splice方法。对于对象来说,在数据初始化后添加新的属性也是检测不到的。0.11版提供了每个$add方法:_.isReserved(key)){this[key]=valreturn}ob.convert(key,val)if(ob.vms){vari=ob.vms.lengthwhile(i--){varvm=ob.vms[i]vm._proxy(key)vm._digest()}}else{ob.notify()}})直接调用convert方法即可。设置完成后,通知更新分为两种情况。如果设置了data的根属性,那么需要将该属性委托给Vue实例,并通知该实例及其所有子实例的watcher。做一个强制更新。如果不是根属性,则调用对象的观察者实例的notify方法通知对象对应属性的订阅者进行更新。数据观察到这里就结束了,但是还不知道什么时候收集依赖,什么时候赋值Observer.target,如果有数据更新,什么是watcher,watcher是怎么触发DOM的?更新和如何更新,还有很多问题,下次再见。