Vue 源码系列之 computed 的初始化和更新

时间:2021-2-20 作者:admin

从官方文档里我们能看到,当我们需要在模板里,依赖某些 data 做一些复杂操作得到一个结果的时候,可以使用计算属性 computed。当 data 改变时,计算属性的值也会产生改变,然后更新视图。官方文档里还提到了,只有当计算属性依赖的响应式数据发生改变时,才会重新计算计算属性的结果,所以计算属性还具有缓存值的特性。

所以,这篇文章就从源码来看看,Vue 是怎么实现 computed 的这些特性的。

初始化

首先来看看 computed 的初始化部分。

computed 的初始化发生在 Vue 实例化时(源码地址) 执行的 initState 方法.

// vue/src/core/instance/init.js
...
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm)
    initState(vm) // 初始化 state,包括 data/props/computed/methods/watch
    initProvide(vm)
    callHook(vm, 'created')
    ...
  }
}
...

接下来再看一下 initState(源码地址) 做了什么:

// vue/src/core/instance/state.js
...
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化 computed,并传入 vm 实例和用户自定义 computed 对象
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
...

从源码中可以看到,initState 里面执行了一些状态(data/props/methods/computed/watch)相关的初始化操作,然后我们找到 initComputed(源码位置同 initState),再去看看 computed 的初始化都做了什么。

// vue/src/core/instance/state.js
...
const computedWatcherOptions = { lazy: true } // computedWatcher 的配置对象

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 函数接收了两个参数,一个是实例本身,第二个是用户定义的 computed 对象。函数内首先是创建了一个 watchers 的空对象,然后遍历用户定义的 computed 对象,对用户定义的每一个 computed 进行判断,如果值是函数,该函数直接作为 getter 存储起来,如果值是对象,则取对象内的 get 属性作为 getter 存储起来。接着为每一个 computed 创建一个 Watcher 实例,将之前保存的 getter 函数和 vm 实例,以及计算属性的 computedWatcherOptions 作为参数传给 Watcher,最后将 Watcher 的实例存储到 watchers 对象内,而 Watcher 是做什么的,我们后边会有分析,现在只需要知道有这一步即可。最后的最后,判断 computed 是否已经存在于 vm 实例中,如果存在,判断是和 data/props 中的谁冲突,然后给出对应的冲突提示,如果不存在,则执行 defineComputed 函数。

// vue/src/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 函数里面,定义了计算属性的 settergetter,同时将计算属性绑定到 vm 实例上。在 defineComputed 中对于 setter 的设置仅仅只是赋值而已,主要看看对于 getter 的设置函数 createComputedGetter

// vue/src/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 主要是返回 computed 的 getter 函数。到此,computed 的初始化过程其实已经结束了,那么他的三个特性到底是怎么实现的呢?还记得之前提到的 watchers 对象吗?个中奥秘就在这个对象里存储的 Watcher 实例中。

computed watcher

从 computed 的 getter 函数可以看到,当 computed 被访问时,其实是执行了 initCompute 函数中保存的 Watcher 实例对象的一些方法。那么 Watcher源码地址) 里面存在什么奥秘呢?

先从源码里看一下 computed 被访问时会使用到的 Watcher 方法和属性。

// vue/src/core/observer/watcher.js
export default class Watcher {
  constructor (
    vm: Component, // vm 实例
    expOrFn: string | Function, // 针对 computed 来说,这里就是 `initComputed` 里传进来的计算属性的 getter 函数
    cb: Function, // `initComputed` 中传进来的是 noop
    options?: ?Object, // `initComputed` 中传进来的 computedWatcherOptions 对象, { lazy: true }
    isRenderWatcher?: boolean // `initComputed` 中未传该参数
  ) {
    ...
    // options
    if (options) {
      ...
      this.lazy = !!options.lazy
      ...
    } else {
      ...
    }
    ...

    this.dirty = this.lazy // for lazy watchers

    ...
  }

  ...

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
  ...
}

watcher 源码里看到,dirty 属性默认是 true,所以当计算属性第一次被访问时,先执行了一次 evaluate 方法,方法内会执行 get 方法,并将 get 方法得到的值保存到 value 属性中,最后修改 dirty 的值为 false。好了,到这里我们就说到计算属性的第一个特性了 – 缓存值。试想一下,如果没有其他的地方修改 dirty 属性的值,那么是不是就意味着,当一个计算属性被访问一次之后,再访问该计算属性时,它的 watcher.dirty 都是 false,也就意味着 evaluate 不再执行,也就不会去重新求值,那么在计算属性 getter 函数的最后返回的 watcher.value 也就是之前保存的结果。就这样一个缓存特性就实现了。

我们紧接着先看一下计算属性的值是怎么计算的。上 get 方法!

// vue/src/core/observer/watcher.js
...

/**
 * Evaluate the getter, and re-collect dependencies.
 */
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
}

...

上边我们说过,get 方法主要是用来求值的,当 get 方法执行的时候,先把当前的 watcher 压入到活动的 watcher 栈(源码位置)里,然后执行 watcher 实例创建时传入的 getter 方法,取得计算属性的结果。那么在执行 getter 方法时,会访问到计算属性中依赖的 data,触发 data 的依赖收集,将计算属性的 watcher 保存到自己的 dep.subs 里。到这里计算属性依赖 data 的特性也就实现了。当 getter 执行完之后,把当前的计算属性 watcher 弹出活动的 watcher 栈,同时通过修改 Dep.target 为当前栈里的第一个 watcher。最后返回取到的计算属性结果。

现在我们再回到上边计算属性的 getter 函数,当计算方法执行完之后,执行 watcher.depend() 方法,watcher.depend 方法其实很简单,遍历存储的 dep,分别执行每个 depdepend 方法,源码(源码位置)如下:

// vue/src/core/observer/dep.js
...

depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}

...

从上边源码能看到,depend 方法只是将 dep 添加到当前的 watcher 中,对应到 computed 这里应该就是访问计算属性的 watcher,再来看一下 addDep 方法的源码(源码位置):

// vue/src/core/observer/watcher.js
...

/**
 * Add a dependency to this directive.
 */
addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

...

addDep 会存储传入的 dep 对象,同时如果这个 watcher (此时的 watcher 是访问计算属性的 watcher)没有保存过这个 dep,那么会将这个 watcher 保存到这个 dep 里。这样就可以保证 data 始终可以收集到所有的观察对象。具体的 data 的收集依赖会在之后陆陆续续的文章中讲到。先抽象的知道一下这里用到了 data 的依赖收集即可。既然 data 收集到了所有的观察者,那么当 data 更新后,会通知到这些所有的 watcher 执行 update方法(源码位置):

// vue/src/core/observer/watcher.js
...

/**
 * 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)
  }
}

...

update 方法会先判断当前的 watcher 是否是延迟执行的或者说是惰性的。那么对于 computed watcher 来说,在创建 watcher 的时候通过传入的 computedWatcherOptions 对象就定义了 computed watcher 是惰性的。那么当 data 被修改,通知 computed watcher 更新的时候,其实只是修改了 computed watcher 的 dirty 属性为 true,告诉计算属性,你已经“变脏”了,下次被访问的时候要重新计算值了。data 被修改,通知 computed watcher 更新以后,会继续通知其他的 watcher,比如访问过计算属性的 render watcher,那么 render watcher 就会执行更新,再次访问 computed,上边 computed 已经被标记为“变脏”了,所以这一次 computed 会重新计算获取新值。

总结一下,计算属性的缓存值特性是通过 watcherdirty 属性来决定的,当计算属性依赖的 data 更新时,会修改计算属性 watcherdirtytrue,当计算属性再次被访问,就会去重新求值,反之,计算属性会直接返回之前保存的值。另外,computed watcher 在 update 时,并不会直接求值,而是当计算属性再次被访问是才会去重新求值。

文章中涉及到的 data 依赖收集、watcherdep 我们会在后边陆陆续续的进行更新,稍安勿躁,很快再来!

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。