翻开vue官网,描述计算属性的getter函数执行时机的只有一句——计算属性的值始终取决于他依赖的响应式属性的值。不知道大家看到这句话的时候会有什么样的概念,我看到的时候,会想一般使用计算属性的时候,就是当计算属性的依赖改变的时候,会相应地触发getter函数重新求值,这点没有问题。但是vue是在初始化的时候,getter函数的执行时机是怎样的呢?之前搞过单片机的笔者想,难道是因为在初始化计算属性所依赖的data属性过程中,产生了什么赋值的“抖动”才执行计算属性的?而且是先求值再来渲染vue的Vnode还是先渲染Vnode再来对计算属性求值呢?本着不明白就拆开看的原则,本次讲解一下计算属性的实现原理。
上次讲到在vue初始化时会执行_init方法,在_init方法中会调用initState来初始化计算属性,执行initComputed函数。
函数中,先为实例初始化_computedWatchers属性。之后遍历computed中的每个计算属性,把定义值保存在userDef中,再取计算属性的求值函数放入getter中,通过这个getter实例化一个watcher实例,这个watcher实例同vue在初始化的时候,是同一个watcher类,但是他是一个计算属性的watcher。再放入函数开始时初始化的watchers数组中,之后通过defineComputed函数来定义计算属性的属性描述get。
至此,我们知道了每个计算属性其实也是有自己的watcher。
但是,我们在initComputed函数上方可以看到计算属性的watcherOption即computedWatcherOptions对象中定义了lazy为true。起到什么作用呢,我们来到Watcher类的构造函数中:
如果有参数options就把watcher实例中的属性赋成相应的值,即lazy属性为true。那这个lazy有什么用呢?我们可以看到watcher构造函数的最后,
如果定义了lazy为true,那么不同于初始化vue实例的时候初始化的watcher,直接执行mount函数,而是会稍后在vue实例渲染Vnode时,需要读取计算属性的值的时候,才会调用evaluate方法的时候再来进行调用get函数。而且watcher实例中的dirty属性也会与lazy同值。
我们再来看上文中提到的为计算属性的属性描述get返回回调函数的createComputedGetter
在createComputedGetter中,如果watcher.dirty是true,那么就去执行watcher.evaluate(),进而执行watcher中的get函数,在get函数中,开始时把当前的watcherpush到全局的一个watcher栈中,标识当前正在计算的计算属性watcher,然后执行初始化时传进来的回调函数getter函数,并把计算得到的计算属性的值赋给watcher.value。
其实从initState方法中可以看出来
vue是先初始化完了data属性的值再来初始化computed的值,正如上次讲的,在初始化完成data之后,每个data的响应式属性就都被赋予了响应属性get,那么在计算属性对data中的属性进行取值计算的时候,就会调用data属性的get,当前正在计算的计算属性watcher就会自动完成依赖收集的过程,即计算属性的watcher中就可以收集到他的所有依赖的data属性的dep依赖。在计算完成之后,get函数会通过popTarget来从全局watcher栈中弹出当前正在渲染的计算属性的watcher。
接下来createComputedGetter会执行watcher.depend()。把刚才计算属性取值计算时收集到的dep,继续添加到上一次添加到全局watcher栈中的vue实例的watcher中,watcher再为这些dep来添加订阅watcher。这样在计算属性所依赖的任何响应式属性的值改变的时候,也会通知vue实例的watcher进行更新。
回到文章开始时的问题:
1.vue初始化时,计算属性求值是怎样触发的?原来计算属性的第一次求值是通过为每个计算属性添加的属性描述get,来执行计算属性watcher的求值方法,才来执行getter函数的,而不是依赖的data数据变化来触发的。
2.是先求值再来渲染vue的Vnode还是先渲染Vnode再来对计算属性求值呢?通过上面的分析可以看出,在实例化计算属性的watcher的时候,有一个lazy配置,这个lazy配置保证了不会初始化计算属性时,即初始化每个计算属性的watcher时就立即计算属性求值,而是在渲染vue实例的Vnode时,调用计算属性的get属性描述的时候,才会真正的调用getter函数来求值,并且进行依赖收集、添加订阅的流程。