计算属性vs侦听属性(一)
计算属性适合用在模版渲染当中,某个值是依赖了其他响应式对象甚至是计算属性计算而来的。
侦听属性适用在观测某个值的变化去完成一段复杂的业务逻辑。
回忆
computed计算属性的过程 当前渲染Watcher订阅computedWatcher,computedWatcher订阅数据, 数据变化通知cW,cW再通知当前渲染Watcher。
computed计算属性的响应式原理:Vue初始化 执行_init中会执行initState(),initState()函数中会执行initComputed(vm, opts.computed)(实际上在创建自组件,自组件实例化Vue.extend时就会执行initComputed把computed属性定义在自组件原型上,那样不用每个组件都去重新创建computed属性)。故事就从这里开始 ...
initComputed(),遍历computed内每个计算属性,创建计算属性相对应的Watcher实例(即 computed watcher,和我们之前分析的data Watcher稍有不同,会多一个this.dep = new Dep(),computed watcher内会有一个订阅器。并且不会触发watcher.get()进行求值),并执行 defineComputed(vm, key, userDef)。
defineComputed(), 把对computed内计算属性的访问 通过defineProperty访问器属性代理到 createComputedGetter(key) 方法内。
createComputedGetter(),拿到当前计算属性对应的Watcher实例执行Watcher.depend()并返回watcher.evaluate()。
Watcher.depend() ,我们很熟悉,之前在get函数的依赖收集中分析过,通过它,当render函数执行访问到该计算属性时,我们会让渲染Watcher订阅当前computed watcher(即computed watcher的dep中会加入渲染Watcher)。
watcher.evaluate(),执行this.get(),逻辑很简单,做了两件事。1、 把当前Watcher( Dep.target)写为computed Watcher,然后把 this.dirty 设置为 false。2、求值,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的回调函数,过程中一旦去取已定义的响应式属性,当前Watcher( Dep.target)便会订阅该属性(这时Watcher是computed Watcher,属性对应dep实例内就会加入当前Watcher)。3、最后通过 return this.value 拿到计算属性对应的值。
所以一旦计算属性依赖的响应式数据发生变化。触发依赖数据的set函数,派发更新,依次异步执行dep实例中所有订阅该数据的Watcher(包括computed Watcher)的update()方法。
update()会判断,如果有渲染Watcher订阅了这个computed Watcher的变化,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了订阅了该computedWatcherd的渲染 watcher 重新渲染。
Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
computed
计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:
initComputed函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed 的实现:
defineComputed这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:
createComputedGettercreateComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。
例子当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:
初始化 computed watcher 实例可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。
然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:
watcher.depend()注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。
然后再执行 watcher.evaluate() 去求值,来看一下它的定义:
watcher.evaluate()evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,并把当前Watcher( Dep.target)写为computed Watcher,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName。
这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher。
最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:
watcher.update()那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:
watcher.run()getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。
接下来我们来分析一下侦听属性 watch 是怎么实现的。