原稿:语雀 · 【原理五】computed & watch · 原目录:Vue › 【原理五】computed & watch

计算属性

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:

const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
    // ...
  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
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {/*...*/}
  }
}
  1. 函数首先创建 vm._computedWatchers 为一个空对象,接着遍历 computed 对象,获取每个计算属性的 getter 函数(暂不考虑 setter)
  2. 接下来为每一个计算属性 key 创建一个 computed watcher,默认进行惰性观察,设置 value 为 lazy,而不是立刻获取值
// Watcher()
this.value = this.lazy
  ? undefined
: this.get();
  1. 最后调用 defineComputed(vm, key, userDef)
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

defineComputed() 中利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,计算属性有 setter 的情况比较少,重点关注 getter。getter 是 createComputedGetter(key) 的返回值:createComputedGetter 返回一个函数 computedGetter,实际上就是 watchers[] 元素的包装

function createComputedGetter (key) {
  return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {watcher.evaluate();}
        if (Dep.target) {watcher.depend();}
        return watcher.value
      }
  }
}
/**
  * Evaluate the value of the watcher.
  * This only gets called for lazy watchers.
  */
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};
/**
  * Evaluate the getter, and re-collect dependencies.
  */
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  }
  /.../
  return value
};

当首次渲染时,调用 getter:

  • dirty 为真,执行 watcher.evaluate(),通过 watcher.get() 调用 userDef.get,因为计算属性依赖于别的属性,这一步又回去调用其他属性的 getter,根据响应式原理,则 computed watcher 会订阅到依赖属性的变化上
  • 之后执行 depend(),渲染 watcher 订阅到 computed watcher 的变化上
/**  * Subscriber interface.  * Will be called when a dependency changes.  */Watcher.prototype.update = function update () {  /* istanbul ignore else */  if (this.lazy) {    this.dirty = true;  } else if (this.sync) {    this.run();  } else {    queueWatcher(this);  }};

当计算属性依赖的数据修改,触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

  • computed watcher 属于惰性观察,会先设置 this.dirty = true,而不是立刻重新计算
  • 当再次通过 computedGetter 访问这个计算属性时,才会执行 evaluate(),通过 get() 更新计算值

计算属性的缓存即通过上述过程实现,当依赖属性未修改时,this.dirty == false ,调用 computedGetter 时不会执行 evaluate(),而是直接返回缓存的 value


侦听

侦听属性的初始化发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后

if (opts.watch && opts.watch !== nativeWatch) {  initWatch(vm, opts.watch)}

initWatch 的实现

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 方法,否则直接调用 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)
}

首先对 handler 的类型做判断,拿到它最终的回调函数,最后调用 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) {
    cb.call(vm, watcher.value)
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}
  1. 侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数
  2. 接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化一个 user watcher,其 lazy 为 false,则构造函数中就会调用 get(),这个 watcher 会订阅在侦听数据的变化上;当侦听项改变,调用 notify() 通知订阅的 watcher,user watcher 也会调用 watcher.update() ,接着执行 watcher.run(),调用回调函数 cb,并且如果设置了 immediate 为 true,则直接会执行回调函数 cb
  3. 最后返回 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher
Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};

watcher 是贯穿 vue 响应式中的角色

  • initComputed():computed watcher,惰性,只修改 dirty,当 dirty 为 true 时,执行 evaluate() -》 get()
  • initWatch():user watcher,非惰性,立即执行 watcher.get(),调用属性 getter
  • mountComponent():组件 watcher,非惰性,立即执行 watcher.get(),调用属性 getter