由深入浅出 Vue 响应式 (一) 和 (二) 的介绍,我们可以画一个大的代码结构图:
我们已经分析了 initState 中的 initData(图右上部分) ,它会将我们的 data 属性转换为 getter / setter。也分析了 mount 的流程,它会编译你的模板到 render 函数,并且创建一个渲染 watcher 来做响应更新。
computed 属性初始化(绿框部分)处于 initState 的流程,晚于 initData ,但早于 mount 的流程,总的来看是从 new Vue 到 dom 的大流程内。
我再次故意强调这个流程的重要性,因为从 Vue 响应式的角度来看,绕来绕去仍然是两个大流程:从 new Vue 到 dom 的初始化, 数据变化时如何响应(只不过computed 的变化是其依赖的变化,而不是 computed 属性本身)。拆分这两个阶段使得我们更好理解 Vue computed 属性的工作原理。
本文以下面的例子来讲解整个流程:
1 | new Vue({ |
源码分析
初始化
1 | if (opts.computed) initComputed(vm, opts.computed) |
initComputed
我们直接看 initComputed, 位于 src/core/instance/state.js
:
1 | const computedWatcherOptions = { lazy: true } |
首先给 vm 定义一个内部属性 _computedWatchers
。然后对每一个 computed 属性新建一个 watcher。
由于我们只有一个计算属性,那么生成的结果如下:
我们知道在 mount 的流程里,会生成一个渲染 watcher。它和 computed watcher 是不同的,不同点是 computed watcher 是一个 lazy watcher,是不会立即求值的。我们来看代码部分是如何工作的。先简化一下上面的代码:
1 | const computedWatcherOptions = { lazy: true } |
我们在新建 watcher 时传入了 { lazy: true }
。我们再来看下 watcher 部分的构造函数:
1 | if (options) { |
如果计算属性不在 vm 上调用 defineComputed。如果 vm 已经有,比如计算属性和 data、 prop 重复,开发环境会报一个 warning。
defineComputed
1 | export function defineComputed ( |
defineComputed 逻辑比较简单,shouldCache 在非服务端渲染的情况下为 true。那么对’fullName’ 来说,它的 getter 就是 createComputedGetter(key) 生成的 函数。函数的 getter 目前是不执行的,后续我们来了解下它的执行过程。
然后通过 Object.defineProperty(target, key, sharedPropertyDefinition)
直接在 vm上定义一个 fullName。虽然和 data 的proxy 流程不太一样,但是我们同样也可以在 vm 上访问计算属性了。
computed 的初始化流程到此就结束了。
mount
由深入浅出 Vue 响应式 (一)可知:首先我们的模板会被编译成 render 函数:
1 | ƒ anonymous( |
1 | var updateComponent; |
然后我们会执行一个渲染 watcher。渲染watcher 会立即求值,调用 其getter 方法。也就是会执行 updateComponent 方法。在 vm._render() 过程中,会执行我们编译出的 render 函数。这样就会调用我们的 fullName 的 get 访问器:
1 | function computedGetter () { |
由上述流程可知,我们定义了一个 lazy watcher , 那么 watcher 有值,并且 watcher.dirty === true。
然后调用watcher.evaluate 方法。evaluate方法本质上就是调用 get 方法进行求值。求值完成后会将 dirty 重置为 false。
1 | 这里我们也看到了 lazy 的概念,只有在访问到的时候才去求值。 |
1 | get () { |
我们看下 Watcher 的 get 方法,首先会 pushTarget(this)。将当前 lazy watcher 设置为 Dep.target。
然后调用 this.getter.call(vm,vm)。this.getter 就是我们的 函数
1 | fullName() { |
此时会访问 this.firstName 和 this.lastName。走到他们的访问器属性:
1 | get: function reactiveGetter () { |
这时 firstName 和 lastName 便把计算属性的 lazy watcher 添加到自己的依赖收集 dep 里了。
然后执行 popTarget:
1 | export function popTarget () { |
把 dep.Target 重置为渲染 watcher。
1 | if (Dep.target) { |
1 | depend () { |
1 | Dep.prototype.depend = function depend () { |
然后调用 watcher.depend 方法。将渲染watcher 添加到 firstName 和 lastName 的依赖收集 dep 内。
至此,从new Vue 到 dom过程, 依赖收集便做完了。
响应式
当我们将 firstName 改成 ‘li’ 的时候:
1 |
|
会走入 firstName 的 setter 内:
1 | set: function reactiveSetter (newVal) { |
setter 最终调用 dep.notify 方法。
1 | notify () { |
notify 会按顺序调用所收集依赖的 update 方法。我们来看下 update 方法的代码:
1 | update () { |
update 方法,如果遇到 lazy watcher,只会将 dirty 设置为 true。然后就没了。
如上面的流程可知,我们会有两个 watcher, 一个是 lazy watcher ,一个是渲染 watcher。只有渲染 watcher会进入到 watcher 的队列中。
computed 的缓存特性
computed 缓存通常是相对于 method 来说的。computed 只会依赖于其相关 data,而 method 每次都要调用生成。
ISSUE
有问题? 来 GitHub 一起讨论。