【Vue源码】计算属性和监听属性

551 阅读3分钟

本篇文章主要是《浅析Vue.js依赖收集和派发更新中观察者模式的应用》的扩展,主要是介绍计算属性和监听属性,建议先阅读上篇👆。

计算属性 computed

computed的初始化定义在文件中: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属性在SSR期间只是getter
  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) {
      // 为computed属性创建内部watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // 组件定义的计算属性已经在组件原型定义过。 
    // 我们只需要定义在此处实例化定义过的计算属性。
    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属性,然后为每个computed属性创建watcher,并保存在vm._computedWatchers上,最后,若computed的key未在vm上定义过,则执行defineComputed(vm, key, userDef)

下面是defineComputed的实现

function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  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
  }
  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)
}
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
    }
  }
}

defineComputed的核心是为计算属性设置getter,getter的逻辑是找到其watcher,调用其evaluate方法和depend方法,并返回watcher.value

class Watcher {
  // ...
  // 计算watcher的值。只有lazy watchers才需要这样做。
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  
  // 取决于被此watcher收集的所有deps。
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

computed watcher与渲染watcher不同的是lazy: truethis.dirty = this.lazy,所以其dirty= true

如果组件的模板中有使用到computed,依赖收集和派发更新的流程如下:

  • 当组件在挂载时,其渲染watcher在计算时会读取到computed的值时,就会触发computed的getter,然后找到computed watcher,因为dirty= true,所以会执行其evaluate方法;
  • evaluate会执行this.get()重新计算computer的值,在这个过程中,Dep.target是computed watcher,同时会触发computed所有依赖项的getter,然后收集所有依赖项的dep,保存在newDeps中,执行cleanupDeps后,newDeps会保存到deps中,这就是依赖收集的过程;
  • evaluate执行完毕之后,Dep.target变为渲染watcher,这时执行computed watcher的depend方法,会遍历computed watcher的所有deps,并执行dep.denped将渲染watcher保存在所有依赖项的订阅者列表subs中,当依赖项发生变化时,便可以直接通知到对应的渲染watcher进行更新了。

监听属性 watch

监听属性的初始化在initState

function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}


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属性的主要逻辑是遍历每一个watch属性,为每一个key执行createWatcher,因为key的值可以是数组,即是支持多个handler。

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

createWatcher的逻辑比较简单,主要是获取到最终的handler,并返回vm.$watch方法的结果,其定义在文件中:/src/core/instance/state.js

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

$watch方法的核心逻辑是创建Watcher,若设置了immediate,则立即执行回调函数,最后返回取消监听的方法。

通过$watch方法创建的是user watcher,其依赖收集和派发更新的流程如下:

  • 创建user watcher时,在get的过程中会获取到监听属性的值,这时Dep.target是user watcher,监听属性会将这个user watcher收集到其dep的订阅列表中,这就完成了依赖收集;
  • 当监听属性发生变化时,会通知user watcher进行更新,便会执行用户传入的回调函数,这就是派发更新的过程。

如果你想了解Vue的Diff算法,点击《图文并茂地描绘Vue的Diff算法》