Vue源码阅读(八):计算属性与侦听属性

1,197 阅读5分钟

很多时候,我们都不清楚该什么时候使用 Vue 的 computed 计算属性,何时该使用 watch 监听属性。现在让我们尝试从源码的角度来看看,它们两者的异同吧。

computed

计算属性的初始化过程,发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initComputed 函数。该函数的定义在 src/core/instance/state.js中:

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

首先创建一个空对象,接着遍历 computed 属性中的每一个 key, 为每一个 key 都创建一个 Watcher。这个 Watcher 与普通的 Watcher 不一样的地方在于:它是 lazy Watcher。关于 lazy Watcher 与普通 Watcher 的区别,我们待会展开。然后对判断如果 key 不是实例 vm 中的属性,调用defineComputed(vm, key, userDef),否则报相应的警告。

接下来重点看defineComputed(vm, key, userDef)的实现:

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

这里的逻辑也是很简单,其实就是利用 Object.defineProperty给计算属性对应的key值添加 gettersetter。我们重点来关注一下 getter 的情况,缓存的配置也先忽略,最终 getter 对应的是 createdComputedGetter(key)的返回值,我们来看它的定义:

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

createdComputedGetter(key)返回一个函数computedGetter,它就是计算属性对应的 getter

至此,整个计算属性的初始化过程到此结束。我们知道计算属性对应的 Watcher 是一个 lazy Watcher,它和普通的 Watcher 有什么区别呢?由一个例子来分析 lazy Watcher 的实现:

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

当初始化整个 lazy Watcher 实例的时候,构造函数的逻辑有稍微的不同:

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

可以发现 lazy Watcher 并不会立刻求值,而是返回的是 undefined

然后当我们的 render 函数执行访问到 this.fullname 的时候,就出发了计算属性的 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.dirty 属性为 true。会执行 Watcher.evaluate() :

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

这里,会通过调用 this.get() 方法,执行对应属性的 get 函数。在我们的例子中,就是执行:

function () {
      return this.firstName + ' ' + this.lastName
}

这个时候,会触发对应变量firstNamelastName的获取,触发对应的响应式过程。得到了最新的值之后,将 this.dirty 属性设置为false

更加关键的代码在这里:

if (Dep.target) {
        watcher.depend()
}

Vue 实例存在一个 Watcher,它会调用计算属性。计算属性中有 lazy Watcher,它会调用响应式属性。每一个 Watcherget() 方法中,都有pushTarget(this)popTarget()的操作。

在上面的代码中,此时的 Dep.targetVue 的实例 Watcher,此时的 watcher 变量是计算属性的 lazy Watcher,通过执行代码watcher.depend(),将计算属性的 lazy Watcher 关联的 dep 都与 Dep.target 发生关联。

在我们的例子中,即把this.firstNamethis.lastName与实例 Watcher关联起来。这样就可以实现:

  • this.firstNamethis.lastName发生变化的时候,实例 Watcher 就会收到更新通知,此时的计算属性也会触发 get 函数,从而更新。
  • this.firstNamethis.lastName未发生变化的时候,实例 Watcher 调用计算属性,因为 lazy Watcher 对应的 dirty 属性为false,那么就会直接返回缓存的 value 值。

由此可以看出:计算属性中的 lazy Watcher 有以下作用:

  • 保存 computed 属性的 get 函数(方法)
  • 保存计算结果
  • 控制缓存计算结果是否有效(通过 this.dirty 属性)

Watch

侦听属性的初始化过程,与计算属性类似,都发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initWatch 函数。该函数的定义在 src/core/instance/state.js中:

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用createWatcher:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。

最终都会执行const watcher = new Watcher(vm, expOrFn, cb, options)实例化一个 Watcher。这里需要注意的一点是这是一个 user Watcher,因为 options.user = true。通过实例化 Watcher 的方式,一旦我们 watch 的数据发生了变化,它最终会执行 Watcherrun方法,执行回调函数 cb

通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,比如:

run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

关于 Vue 内部的错误处理,有新文章做对应的讨论,可戳这里

总结

就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。


vue源码解读文章目录:

(一):Vue构造函数与初始化过程

(二):数据响应式与实现

(三):数组的响应式处理

(四):Vue的异步更新队列

(五):虚拟DOM的引入

(六):数据更新算法--patch算法

(七):组件化机制的实现

(八):计算属性与侦听属性

(九):编译过程的 optimize 阶段

Vue 更多系列:

Vue的错误处理机制

以手写代码的方式解析 Vue 的工作过程

Vue Router的手写实现