[Vue源码学习] computed

441 阅读4分钟

系列文章

前言

Vue中可以使用计算属性,缓存中间的计算结果,只有在相关响应式依赖发生变化时,它们才会重新求值,从而避免重复的计算,提高性能,那么接下来,就来看看计算属性在Vue中是如何实现的。

computed

在初始化Vue实例的过程中,会调用initState方法处理数据,在该方法中,如果检测到有computed选项,就会调用initComputed方法,处理计算属性,代码如下所示:

/* core/instance/state.js */
export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // ...
  if (opts.computed) initComputed(vm, opts.computed)
  // ...
}

const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  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
    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.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 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)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        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)
      }
    }
  }
}

可以看到,在initComputed方法中,首先定义一个空对象_computedWatchers,用来存放计算属性相关的Watcher实例;然后遍历computed选项,对计算属性进行处理。

因为计算属性支持单个函数,也支持带getset属性的对象,所以首先取得计算属性的取值函数getter,然后在非服务端渲染的情况下,创建计算属性对应的Watcher实例,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    // ...
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // ...
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

可以看到,在创建Watcher实例的过程中,这里的getter就是上面计算属性的getter,而options就是computedWatcherOptions,它的lazy选项为true,所以Watcher实例的lazydirty属性为true,由于lazytrue,所以对于计算Watcher来说,在创建时不会调用get方法进行求值。

回到上面的initComputed方法中,在创建好Watcher后,将其添加到_computedWatchers中。接着判断计算属性是否已经存在于当前Vue实例上,如果不存在,就调用defineComputed方法,将其添加到当前Vue实例上,代码如下所示:

/* core/instance/state.js */
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.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)
}

可以看到,在defineComputed方法中,给当前Vue实例上添加新的访问器属性,其属性名是计算属性的属性名,而在非服务端渲染的情况下,它的get访问器是通过调用createComputedGetter方法创建的,其代码如下所示:

/* core/instance/state.js */
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,而这个新函数就是对应计算属性的get访问器,所以当我们访问该计算属性时,就会执行computedGetter方法,那么接下来,就来看看在访问计算属性时,Vue又做了哪些工作。

computedGetter

当访问计算属性时,会触发计算属性的get访问器,也就是computedGetter方法。在该方法中,首先会从_computedWatchers中取出对应的Watcher实例,然后通过watcher.dirty属性判断该计算属性是否需要重新计算,首次访问时,dirty属性为true,所以会调用evaluate方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  get() {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
}

可以看到,在evaluate方法中,首先会调用watcher.get方法,在调用pushTarget方法后,会将Dep.target指向当前的计算Watcher,然后执行getter函数,也就是我们在配置选项中编写的函数,如果此计算属性依赖于data选项中的数据,那么就会触发该数据的get访问器,所以当前的计算Watcher会将该数据添加到自己的依赖中,同时此数据对应的dep也会将计算Watcher添加到它的观察者列表中,接下来的popTargetcleanupDeps方法的逻辑就和之前相同,最终,将watcher.get方法返回的值赋值给watcher.value,然后将dirty置为false,表示当前计算属性已经是最新值,不用重新计算。

在执行完evaluate方法后,此时计算属性与它所依赖的数据之间就产生了联系,修改所依赖的数据时,就会通知计算属性进行更新。但是在computedGetter方法中,除了调用evaluate方法外,还有一段逻辑,如果此时Dep.target存在,就会调用watcher.depend方法,再进行一次依赖收集,可以想到这样的一个场景,在组件的渲染过程中,Dep.target首先会指向渲染Watcher,当访问计算属性时,会将渲染Watcher推入targetStack栈中,调用完getter方法后,又会将Dep.target指回渲染Watcher,由于此时Dep.target存在,所以就会执行watcher.depend方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

/* core/observer/dep.js */
export default class Dep {
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}

可以看到,在depend方法中,这里的this指向的还是计算Watcherthis.deps中保存的是计算属性依赖的数据,当执行dep.depend方法时,这里的Dep.target指向的却是渲染Watcher,所以又会在渲染Watcher和数据之间构建联系。

最终,在访问计算属性时,计算Watcher和渲染Watcher都会收集对此数据的依赖,所以当此数据发生变化时,会同时通知计算Watcher和渲染Watcher进行更新操作。那么接下来,就来看看计算Watcher是如何进行更新的。

update

当依赖的数据发生变化时,就会遍历观察者集合,调用它们的update方法,代码如下所示:

/* core/observer/watcher.js */
export default class Watcher {
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

可以看到,对于计算Watcher来说,由于它的lazy选项为true,所以它只会将dirty属性置为true,表示该计算属性需要重新进行计算,但不会将计算Watcher推入watcher queue中。这么做的好处是,如果更新后的渲染Watcher不依赖于该计算属性,就不用执行计算属性的重新求值了,只有当下次又访问到该计算属性时,由于在之前已经将dirty属性为true,所以才会执行计算属性的重新计算。

总结

Vue中,可以通过计算属性对计算的结果进行缓存,避免重复计算的开销。在计算Watcher收集依赖的过程中,会将依赖的数据同步代理到上级Watcher中,所以在数据发生变化时,才会执行组件的重新渲染。