vue computed实现原理

115 阅读2分钟

我们知道计算属性的结果是会被缓存的,除非它依赖的响应式 property 变化才会重新计算。比如我们下面的fullName属性,只有它依赖的surName或者name发生变化,才会重新计算。 例1:

  computed: {
    fullName() {
      return this.surName + this.name
    }
  }

那么它的这个缓存功能究竟怎么实现的呢?今天我们就来探究一下

首先是computd的初始化过程:

computed的初始化发生在data初始化之后,通过initComputed函数完成

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 */)
  }
  /* computed初始化看这里 */
  if (opts.computed) initComputed(vm, opts.computed)

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

initComputed函数会遍历我们传入的computed配置项,并把每个computed创建成一个个计算属性watcher,同时添加到vm实例的_computedWatchers属性上。

注意new Watcher时传入的computedWatcherOptions配置项,它是实现缓存的重要标志

watcher创建工作完成后,它通过defineComputed函数把我们配置项里的每一个computed都添加到vm实例上,同时定义了它们的存取描述符,即给每个属性都添加了getter、setter用于数据劫持

const computedWatcherOptions = { lazy: 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
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )
  }

    /* 通过Object.defineProperty 把计算属性定义到vm实例上, 并对访问和设置进行劫持 */
   if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
}

当我们访问计算属性时(vm.fullName) 会触发computedGetter函数, computedGetter函数根据watcher.dirty判断当前数据是否是脏数据(dirty为true表明是脏数据 需要重新计算)如果是脏数据则执行evaluate方法, evaluate方法只做两件事,一个是执行watcher的get方法获取value,一个是把dirty属性重置为false,以表明当前数据不是脏数据

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

export function defineComputed (
  target: any, 
  key: string,
  userDef: Object | Function
) {
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } 

  Object.defineProperty(target, key, sharedPropertyDefinition)
}


下面是watcher实现的具体过程


let uid = 0
export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.lazy = !!options.lazy
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.dirty = this.lazy // for lazy watchers
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } finally {
      popTarget()
      this.cleanupDeps()
    }
    return value
  }


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

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

}

这里我同样只保存了和缓存相关的代码,我们在创建wather时传入的options里有{laze: true}选项,所以我们的计算属性watcher默认lazy和dirty为ture,所以在我们在new Watcher时不会直接调用get方法,这和渲染wather不同。 这也是为什么当我们的computed刚初始化完成时,这个计算属性watcher的value为undefined。

当我们第一次去访问这个属性时(vm.fullName)会触发computedGetter方法,由于计算属性的dirty默认为true,所以会执行watcher.evaluate()方法,evaluate方法会调用get求值,得到value,同时置dirty为false。

当第二次去访问这个属性时,同样会触发computedGetter,但由于此时dirty为false,所以不会 执行evaluate,而是直接返回watcher.value 这就是使用了缓存

当我们的计算属性watcher所依赖的property发生变化后首先会被响应式系统劫持,然后派发更新,所谓派发更新其实就是依次调用所有依赖了这个属性的wather的update方法,update方法首先会判断你的lazy是不是为true,如果为true的话,把dirty属性置为true。那么当我们再去访问vm.fullName属性时就又会像我们第一次访问属性时一样执行evaluate方法。