理解Vue中computed缓存的实现原理
本文主要通过下面的例子来讲解computed初始化和更新的过程,看看computed属性是如何缓存的,依赖是如何收集的。{{sum}}
初始化computedvue时,先执行init方法,里面的initState会初始化计算属性if(opts.computed){initComputed(vm,opts.computed);}下面是initComputed的代码varwatchers=vm._computedWatchers=Object.create(null);//依次为每个计算属性定义一个计算观察器for(constkeyincomputed){constuserDef=computed[key]watchers[key]=newWatcher(vm,//实例getter,//传入的评估函数theusersumnoop,//回调函数可以忽略{lazy:true}//声明lazy属性来标记computedwatcher)//用户在调用this.sumdefineComputed(vm,key,userDef)}时会发生什么每个计算属性对应的计算观察者的初始状态如下:{deps:[],dirty:true,getter:?sum(),lazy:true,value:undefined}可以看到一开始它的值是undefined,而lazy是true,说明它的值是惰性计算的,dirty只有在模板中读到它的值后才会计算,实际上是属性缓存的关键,先记住了。接下来我们看一下比较关键的defineComputed,它决定了用户读取到计算属性this.sum的值后会发生什么。继续精简,排除一些不影响流程的逻辑。Object.defineProperty(target,key,{get(){//从刚才提到的组件实例中获取computedwatcherconstwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){//只有dirty才会re-evaluatedif(watcher.dirty){//这里会被求值,会调用get,会设置Dep.targetwatcher.evaluate()}//这个也是一个关键,下面会讲到if(dep.target){watcher.depend()}//最后返回计算出的值returnwatcher.value}}})这个函数需要仔细看,它做了几件事,我们用初始化过程来解释一下:首先,dirty的概念代表脏数据,说明这个数据需要再次调用用户传入的sum函数来求值。暂时不关心更新时的逻辑。当第一次在模板中读取{{sum}}时,它必须为真,因此初始化将进行评估。evaluate(){//调用get函数求值this.value=this.get()//标记dirty为falsethis.dirty=false}这个函数其实很清楚,它先求值,然后把dirty设置为false.回头看刚才Object.defineProperty的逻辑,下次没有特殊情况读取sum时,发现dirty为false。我们应该只返回watcher.value的值吗?这其实就是计算属性缓存的概念。依赖收集初始化完成后,会调用render进行渲染,render函数会作为watcher的getter,此时的watcher就是渲染watcher。updateComponent=()=>{vm._update(vm._render(),hydrating)}//创建渲染观察器。renderingwatcher在初始化的时候,会调用它的get()方法,也就是render函数,收集依赖newWatcher(vm,updateComponent,noop,{},true/*isRenderWatcher*/)查看get方法inwatcherget(){//将当前watcher放在栈顶,并设置为Dep.targetpushTarget(this)letvalueconstvm=this.vm//调用自定义函数会访问this.count,从而访问它的getter方法。下面会讲到value=this.getter.call(vm,vm)//evaluation结束之后,当当前watcher出栈时popTarget()this.cleanupDeps()returnvalue}当getter的renderingwatcher被执行(renderfunction),会访问this.sum并触发计算属性的getter,定义在initComputed这个方法中,获取到sum绑定的calculationwatcher后,会调用它的evaluate方法,因为dirty为true在初始化的时候,最后调用它的get()方法把calculationwatcher放到栈顶。这时,Dep。目标还为此计算观察者。然后调用它的get方法,它会访问this.count,触发count属性的getter(如下),将当前Dep.target中存储的watcher收集到count属性对应的dep中。此时,评估结束,调用popTarget()将观察者弹出堆栈。此时最后一个渲染观察者在栈顶,Dep.target又是渲染观察者。//在闭包中,为键计数定义的depconst将被保留。dep=newDep()//最后一个set函数设置的vallet也会保存在闭包中。valObject.defineProperty(obj,key,{get:functionreactiveGetter(){constvalue=val//Dep.target此时计算watcherif(Dep.target){//收集依赖dep.depend()}返回值},})//dep.depend()depend(){if(Dep.target){Dep.target.addDep(this)}}//watcher的addDep函数addDep(dep:Dep){//这里a一系列的去重操作被简化//这里会把count的dep存入自己的depsthis.deps.push(dep)//以watcher本身为参数//返回dep的addSub函数dep.addSub(this)}classDep{subs=[]addSub(sub:Watcher){this.subs.push(sub)}}通过这两段代码,将计算出来的watcher通过dep绑定到属性上进行收集。Watcher依赖于dep,dep也依赖于watcher。它们之间这种相互依赖的数据结构使得很容易知道哪个dep依赖于一个watcher,一个dep又依赖于哪些watchers。然后执行watcher.depend()//watcher.dependdepend(){leti=this.deps.lengthwhile(i--){this.deps[i].depend()}}记住刚才计算的watcher的shape?它在其部门中存储计数部门。也就是说count上的dep.depend()会再次被调用已经是renderingwatcher了,所以这个count的dep会把renderingwatcher存放在自己的subs中。最后收集count的dependencies,它的dep是:{subs:[sum'scalculationwatcher,renderingwatcher]}Dispatchupdate那么我们就到了这道题的重点了。这时候count更新了。如何触发视图更新?回到count的响应式劫持逻辑://闭包中会保留count的key定义的depconstdep=newDep()//闭包也会保留上次set函数vallet设置的值valObject.defineProperty(obj,key,{set:functionreactiveSetter(newVal){val=newVal//触发count的dep的notifydep.notify()}})})好了,下面就是我们刚刚精心准备的notify计数部门的功能。classDep{subs=[]notify(){for(leti=0,l=subs.length;i