Vue源码学习3.6:计算属性computed

226 阅读7分钟

建议PC端观看,移动端代码高亮错乱

关于 computed@2.5 之前的旧版本中的实现可以参考黄轶老师的文章

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中:

export function initState (vm: Component{
  // ...
  if (opts.computed) initComputed(vm, opts.computed)
  // ...
}

initComputed 的定义在 src/core/instance/state.js 中:

1. 初始化过程

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

    // 对于组件来说,在创建子组件构造函数时已经调用了 defineComputed,并将 computed 定义在其原型上
    // 只有对于当根实例来说,才会执行此处的 defineComputed
    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)
      }
    }
  }
}
  • 创建 vm._computedWatchers 为一个空对象,用来保存 computed watcher
  • computed 对象做遍历,拿到计算属性的getter
  • 为每一个 getter 创建一个 computed watcher
  • 判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef)

1.1 实例化computed watcher

实例化时传入构造函数的 4 个参数:

// src/core/instance/state.js
watchers[key] = new Watcher(
  vm,
  getter || noop,
  noop,
  computedWatcherOptions
)
  • getter:表示计算属性的 getter
  • computedWatcherOptions:一个配置对象{ lazy: true },表示这是一个 computed watcher

看下构造函数的逻辑稍有不同:

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  this.value = this.lazy
      ? undefined
      : this.get()
}  

和渲染 watcher 不一样的是,由于我们传入配置的 lazytrue,所以不会立刻调用 this.get() 进行求值

1.2 defineComputed

注意这里 Vue 有一个优化处理,在创建组件构造函数时:

// src/core/global-api/extend.js
Vue.extend = function (extendOptions: Object): Function {
  // ...
  if (Sub.options.computed) {
    initComputed(Sub)
  }
  // ...
}

function initComputed (Comp{
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

这里提前调用了 defineComputed,并且第一个参数传入的是组件的原型,也就是 Comp.prototype

这样做的目的就是避免多次实例化同一组件时,在实例上重复调用 defineComputed 方法。

下面来看看 defineComputed 函数的定义:

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
{
  if (typeof userDef === 'function') {
    // 简化后的
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    // 简化后的
    sharedPropertyDefinition.get = userDef.get
      ? createComputedGetter(key)
      : 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)
}

这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 gettersetter

在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:

// 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 返回一个函数 computedGetter,它就是计算属性对应的 getter

整个计算属性的初始化过程到此结束,下面结合例子和过程来分析。

2. 过程分析

有如下例子:

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

2.1 依赖收集

当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,也就是在 createComputedGetter 中返回的 computedGetter

function computedGetter ({
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

首先拿到这个计算属性的 computed watcher

这里会对 watcher.dirty 进行判断,dirty 是用来标志是否已经执行过计算结果,这是因为只有在相关响应式数据发生变化时,computed 才会重新求值,其余情况多次访问计算属性的值都会返回之前计算的结果,这就是缓存的优化。

2.1.1 watcher.evaluate

当首次执行时,dirtyfalse,因此调用 watcher.evaluate 进行求值,evaluate 函数如下:

/**
 * Evaluate the value of the watcher.
 * This only gets called for lazy watchers.
 */

evaluate () {
  this.value = this.get()
  this.dirty = false
}
  • 执行 this.get() 进行求值。
  • dirty 置为 false,当下次访问 computed 时,可以直接取 watcher.value,达到缓存目的。

在执行 this.get() 进行求值的过程中会执行 value = this.getter.call(vm, vm),这实际上就是执行了用户定义的计算属性的 getter 函数,在我们这个例子就是执行了:

 return this.firstName + ' ' + this.lastName

由于 this.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher,同时也将 computed watcher 添加到了 depsubs 队列中。

2.1.2 watcher.depend

回到计算属性的 getter中接着往下执行

function computedGetter ({
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

这时会执行 watcher.depend 进行依赖的收集:

/**
 * Depend on all deps collected by this watcher.
 */

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

此时 deps 保存的是 this.firstNamethis.lastName 对应的 dep。然后调用他们的 depend 方法。由于当前 Dep.target 恢复成 渲染 watcher 了,所以就构造了 渲染 watcherdep 之间的关系。

最后通过 return watcher.value 拿到计算属性对应的值。

2.2 派发更新

派发更新的条件是 computed 中依赖的数据发生改变,在本例中就是 this.firstNamethis.lastName 发生改变。下面来看看如果 this.firstName 发生改变时发生了什么。

  • 会调用 this.firstNamesetter,进而执行 dep.notify
  • 当执行 dep.notify 方法时,会遍历 subs 数组,然后依次调用 sub.update。在本例中 this.firstNamedep.subs 数组如下 [computedWatcher, 渲染watcher]

2.2.1 computed watcher 执行 update

当执行 computed watcherupdate 方法时:

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

因为对于 computed 来说,lazy 返回,所以 update 过程不会执行状态更新的操作,只会将 dirty 标记为 true

2.2.2 渲染 watcher 执行 update

当执行 渲染 watcherupdate 方法时:

会执行 updateComponent 进行视图重新渲染,而 render 过程中会访问到计算属性,此时由于 this.dirty 值为 true,所以又会对计算属性重新求值。