Vue Computed 代码走读

1,785 阅读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 依赖的数据变化的时候,会触发 Watcherupdate

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

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