vue2.0源码解读|vue2.0源码解读 - 计算属性computed

计算属性 VS 侦听属性 Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
computed 计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:
const computedWatcherOptions = { lazy: true }function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) // 创建空对象 // computed properties are just getters during SSR const isSSR = isServerRendering()for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get // 是否是函数 或者有 get方法 if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) }if (!isSSR) { // create internal watcher for the computed property. // internal内部 watchers[key] = new Watcher( vm, getter || noop, // noop 空函数 noop, // noop 空函数 computedWatcherOptions // lazy: true ) }// component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef) // userDef = computed[key] 函数 } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { // 计算属性是否在 data 或者 props 中存在 warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } }

函数首先创建 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 的实现:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }

export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() // 服务端渲染 if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache // 上面定义 false ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) // 下面 : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } Object.defineProperty(target, key, sharedPropertyDefinition) }

这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 gettersettersetter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }

createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。
var vm = new Vue({ data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } })

当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同
// 跟本地源码不太一样 constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { // ... if (this.computed) { this.value = https://www.it610.com/article/undefined this.dep = new Dep() } else { this.value = this.get() } }

可以发现 `computed watcher` 会并不会立刻求值,同时持有一个 `dep` 实例。 然后当我们的 `render` 函数执行访问到 `this.fullName` 的时候,就触发了计算属性的 `getter`,它会拿到计算属性对应的 `watcher`,然后执行 `watcher.depend()`,来看一下它的定义:

/** 跟本地代码不同 * Depend on this watcher. Only for computed property watchers. */ depend () { if (this.dep && Dep.target) { this.dep.depend() } }

注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。
然后再执行 watcher.evaluate() 去求值,来看一下它的定义:
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. // 只为计算属性量身打造 */ evaluate () { this.value = https://www.it610.com/article/this.get() this.dirty = false // 视频版本有返回 this.value }

evaluate 的逻辑非常简单,通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = https://www.it610.com/article/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() 方法:
/** * Subscriber interface. * Will be called when a dependency changes. // */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }

/** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = https://www.it610.com/article/this.get() if ( // 当当前计算的 value 和 上一次的value相同时,则什么都不做,否则当值一样时,仍然执行getter,会重新出发渲染,造成渲染浪费,计算成本是较低的,而重新渲染成本则较高 value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher"${this.expression}"`) } } else { // 重新出发更新 this.cb.call(this.vm, value, oldValue) // this.cb = this.deps.notify() // 视频代码中 callback } } } }

【vue2.0源码解读|vue2.0源码解读 - 计算属性computed】函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变花才会触发渲染 watcher 重新渲染,本质上是一种优化。

    推荐阅读