Vue Computed 代码走读

1,823 阅读3分钟

废话不说,直接看代码:

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 (!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)
    }
  }
}

这里遍历了 computed 里面的每一个属性,并且为每一个属性初始化了一个 Watcher 对象。这样,当我们在 computed 里面访问 data 里面的属性时,就可以收集到依赖了。注意到这里传入了 { lazy: true },我们看看会有什么效果:

    this.dirty = this.lazy // for lazy watchers
    ...
    this.value = this.lazy
      ? undefined
      : this.get()

该属性仅仅是标记了当前数据是 “脏的”,并且不会立即求值。所谓 “脏的” 指的是当前值已经脏了,需要重新求值了,这个后面会再提到。

然后我们看看 defineComputed 做了啥:

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 不考虑服务端渲染,这里为 true
  const shouldCache = !isServerRendering()
  // 只看 computed 值为函数的情况
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

这里执行了 createComputedGetter 这个方法:

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
    }
  }
}

当我们第一次访问计算属性的时候会触发 get,由于 dirty 为 true,所以这里会走 watcher.evaluate 进行求值,并将 this.dirty 置为 false,这样下次再对 computed 进行求值的时候就不会执行 watcher.evaluate() 了,这样就实现了缓存功能。

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

而当 computed 依赖的数据变化的时候,会触发 Watcher 的 update:

  update () {
    /* istanbul ignore else */
    // computed
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 入队
      queueWatcher(this)
    }
  }

这里仅仅是把 dirty 又重置为了 true 以使得下次对 computed 进行求值的时候重新执行 watcher.evaluate()。

缓存功能分析完了,我们来看看下面这两段段代码做了什么:

if (Dep.target) {
  watcher.depend()
}
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

这里有点难理解,我们用一个例子来说明:

首次渲染的时候组件会实例化一个 Watcher 对象,同时会触发对 description 的求值,这里又会实例化一个 Watcher,而 description 中对 fullName 进行求值,又会实例化一个 Watcher。这样就形成了一个依赖栈,靠近栈底的元素会依赖其上面的元素。

当执行 fullName 的时候,由于其依赖了 firstName 和 secondName,所以它会被添加进两者的 dep 中。收集完后会执行 popTarget(),此时 Dep.target 指向 description 的 Watcher,然后会执行 watcher.depend() 。注意这里的 watcher 还是 fullName 的,即 fullName 依赖啥,其他依赖 fullName 的 Watcher 也需要跟它有同样的依赖。举个例子:儿子依赖老爸,老爸是个啃老族依赖父母,所以孙子也间接依赖了爷爷奶奶。同样的,组件的 Watcher 也是同理。

我们调试下这段代码,发现跟我们的分析是一致的: