吃透Vue计算属性
前言一提到计算属性,我们马上就会想到它的一个特性:缓存,而Vue文档中也说:计算属性是根据其响应式依赖进行缓存的。那么计算属性是如何缓存的呢?那么,计算属性的观察者是如何进行依赖收集的呢?接下来,让我们深入了解一下原理。本文需要对响应式的基本原理和Observer、Dep、Watcher等几个关键角色有一定的了解。从一个简单的例子开始:{{this.text}}
constvm=newVue({el:'#app',data(){return{name:'xiaoming',}},computed:{text(){return`Hello,${this.name}!`}},方法:{changeName(){this.name='onlyil'},},})先显示你好,小明!,再显示你好,onlyil!vue初始化还是从vue初始化开始,从newVue()开始,构造函数会执行this._init。在_init中会执行合并配置、初始化生命周期、事件、渲染等,最后会执行vm.$mount挂载。//src/core/instance/index.jsfunctionVue(options){//...this._init(options)}//src/core/instance/init.jsVue.prototype._init=function(options?:Object){//合并选项//...//初始化系列//...initState(vm)//...//mountif(vm.$options.el){vm.$mount(vm.$options.el)}}Computed属性在initState中初始化://src/core/instance/state.jsexportfunctioninitState(vm:Component){constopts=vm.$options//...//Initializecomputedif(opts.computed)initComputed(vm,opts.computed)//...}computed初始化看initComputed:functioninitComputed(vm,computed){constwatchers=vm._computedWatchers=Object.create(null)//遍历计算选项,并依次定义它们for(constkeyincomputed){constgetter=computed[key]//为计算属性创建一个内部观察者watchers[key]=newWatcher(vm,getter||noop,//computedpropertytextfunctionnoop,computedWatcherOptions//{lazy:true},指定lazy属性,表示要实例化computedWatcher)//为计算属性定义getterdefineComputed(vm,key,userDef)}}1.定义_computedWatchers首先定义一个watchers空对象,挂在vm._computedWatchers上,用来存放vm实例中所有的computedWatcher2。实例化computedWatcher以遍历计算选项并实例化watcher。参数中的getter为上例中computedpropertytext对应的函数:属性,指示要实例化computedWatcher。实例化computedWatcher:classWatcher{constructor(vm,expOrFn,cb,options){//optionsis{lazy:true}if(options){//...this.lazy=!!options.lazy//...}this.dirty=this.lazy//对于惰性观察者,初始dirty为真this.getter=expOrFn//lazy为真,不进行求值,直接返回undefinedthis.value=this.lazy?未定义:这个。get()}}执行构造函数时指定lazydirty为true,最后执行watcher.value=undefined而不执行get方法求值。什么时候评价?我们稍后会知道。回到上面,在为计算属性创建内部观察者之后观察者对象看起来像这样:{text:Watcher{lazy:true,dirty:true,deps:[],getter:function(){${这个。name}!`},value:undefined,//直接赋值undefined,}}3.定义计算属性的getter,看看defineComputed做了什么:]if(watcher){if(watcher.dirty){watcher.evaluate()}if(Dep.target){watcher.depend()}returnwatcher.value}}})}你熟悉吗?Object.defineProperty方法还用于在定义响应式数据的defineReactive方法中定义访问器属性。这里在vm实例上定义了text属性,只要访问this.text就会执行相应的getter,所以暂时不用看这个函数做了什么。this.text什么时候会被读取?答案如下。总结一下computed初始化过程:定义vm._computedWatchers存放vm实例的所有computedWatcher,遍历computed选项并实例化watcher,不求值,直接设置watcher.value=undefined通过defineComputed定义computed属性的getter,以及等待后面读取第一次渲染初始化完成后,会进入mount阶段,执行render时会读取计算出的属性文本,生成vnode。本文中的render函数如下:functionrender(){varh=arguments[0];returnh("div",[h("h2",[this.text]),//这里读取计算属性文本h("button",{"on":{"click":this.changeName}},["更改名称"]),]);}1.触发计算属性的getter会触发计算属性的getter,也就是上面定义的accessor属性:get:function(){constwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){//dirty此时为true,evaluateif(watcher.dirty){//evaluate,收集对数据的依赖,让computedWatcher订阅数据//这里的数据是"this.name"watcher.evaluate()}if(Dep.target){watcher.depend()}returnwatcher.value}}2.评估watcher.evaluate()取出vm._computedWatchers中对应的watcher,此时watcher.dirty为true,执行watcher.evaluate()//watcher.evaluateevaluate(){this.value=this.get()this.dirty=false}这个函数做了两件事:执行get去求值,这里就是回答上面什么时候求值的问题;将脏设置为假。先看求值://watcher.getget(){pushTarget(this)//Dep.target设置为当前computedWatcherletvalueconstvm=this.vmtry{//触发响应数据的依赖集合value=这个。吸气剂。call(vm,vm)}catch(e){//...}finally{popTarget()//Dep.target重置为renderingwatcher}returnvalue}这里需要知道一个pre-content,全局的Dep.targetStored是当前被评估的watcher,由一个栈targetStack维护。当前处于渲染过程中,所以栈是这样的:[renderingwatcher]。functionpushTarget(target:?Watcher){targetStack.push(target)Dep.target=target}首先pushTarget(this),Dep.target成为当前的computedWatcher,栈看起来是这样的:[renderwatcher,computedWatcher]。然后执行getter以触发响应数据的依赖集合。再回顾一下getter:function(){return`Hello,${this.name}!`}显然执行这个函数会读取到th??is.name,name的dep会收集当前的computedWatcher,订阅当前的computedWatchernameChanges(这里不做详细描述,参考响应式原理的依赖集合)。收集完成后执行popTarget(),Dep.target再次成为renderingwatcher,此时的stack是这样的:[renderingwatcher]。然后返回计算出的值并将dirty设置为false。此时name的dep是这样的:{id:3,subs:[computedWatcher],//收集到的computedWatcher}computedWatcher是这样的:{dirty:false,//求值完成,dirty设置为falsedeps:[namedep],//订阅了namevalue:"Hello,xiaoming!",}3.watcher.depend()此时这里执行计算属性的get访问:get:function(){constwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){//evaluation...//这里执行if(Dep.target){watcher.depend()}returnwatcher.value}}这时候,dep.target正在渲染watcher,所以watcher.depend()会被执行。//watcher.dependdepend(){leti=this.deps.lengthwhile(i--){this.deps[i].depend()}}可以看到computedWatcher订阅的deps依次执行dep.depend,熟悉响应性原则应该马上就知道了。这是收集对这些dep的响应数据的依赖,也就是收集对example中name的依赖。谁在收集它们?上面提到此时的Dep.target是渲染watcher,那么总结一下,这一步所做的就是:让computedWatcher订阅的响应式数据集合渲染watcher。这一步之后,name的dep是这样的:{id:3,subs:[computedWatcher,renderingwatcher],//收集渲染watcher}最后返回watcher.value,get访问结束,render函数继续往下,然后渲染最终页面。触发更新当按钮被点击时,执行this.name='onlyil',会触发name的accessor属性集,执行dep.notify(),依次触发其收集的watchers的更新逻辑,即[computedWatcher,renderingwatcher]更新。1.触发computedWatcherupdate//watcher.updateupdate(){//computedWatcher的lazy为trueif(this.lazy){this.dirty=true}//...}只做一件事,即设置dirty如果为真,则表示计算出的属性是“脏”的,需要重新计算。什么时候重新评估,见下文。2.触发renderingwatcherupdate//watcher.updateupdate(){//...//queueWatcher(this)}这里是加入异步更新队列,最后执行render函数生成vnode,同第一次渲染,在render过程中,会再次读取computedattributetext,再次触发其getter:get:function(){constwatcher=this._computedWatchers&&this._computedWatchers[key]if(watcher){//dirty此时为真,"Dirty"if(watcher.dirty){//重新求值watcher.evaluate()}//...returnwatcher.value}}重新求值会重新执行我们定义的函数在computedoption,page的新值Hello,onlyil!function(){return`Hello,${this.name}!`}至此更新结束。如何缓存通过以上流程分析,我们可以得出以下结论:首次渲染时实例化computedWatcher并定义属性dirty:false,在渲染过程中评估和收集依赖;当computedWatcher订阅的响应数据为namechanges时,触发computedWatcher的更新,修改dirty:true;render函数执行时读取计算属性文本,发现dirty为true,重新求值,更新pageview。可以发现一个关键点,computedWatcher的update只做了一件事:修改dirty:true,evaluation操作一直在render进程中。现在我们修改示例并添加数据计数和方法添加:
{{this.text}}
{{this.count}}
constvm=newVue({el:'#app',data(){return{name:'xiaoming',count:0,}},computed:{text(){return`Hello,${this.name}!`}},methods:{changeName(){this.name='onlyil'},add(){this.count+=1},},})点击Add按钮,count会改变,那么computedWatcher会不会在重新渲染的时候重新求值?答案是否定的,缓存的关键就在于此。回头看文章开头引用Vue文档中的一句话:Computedpropertiesarecachedbasedontheirresponsivedependencies。结合上面的分析,我恍然大悟,原来计算属性文本的getter函数并没有读取count,所以它的computedWatcher不会订阅count的变化,也就是count的dep不会收集到计算观察者。所以当count发生变化时,并不会触发computedWatcher的更新,dirty依然为false,说明这个computed属性不是“脏”的。那么在后续的渲染过程中读取计算属性文本时,就不会重新计算了,起到了缓存的作用。后记整个过程还是有点费脑子,但多做几次,你就会慢慢体会到computedimplementation的巧妙之处。其实缓存的原理很简单,就是一个flag~?注:文中代码已删,最好结合源码断点。