computed数据缓存详解

1,997 阅读3分钟

在源码层面上解读computed如何进行缓存的。

vue源码src\core\instance\state.js中会对参数computed进行初始化源代码如下:(已经进行简化处理)

const computedWatcherOptions = { lazy: true }
function initComputed(vm, computed) {
  // 声明变量,在vm上添加_computedWatchers属性
  const watchers = vm._computedWatchers = Object.create(null)
  // 循环
  for (const key in computed) {
    // 保存用户设置的计算属性定义
    const userDef = computed[key]
    // 如果userDef是函数就将userDef赋值给ta,不是将userDef.get赋值给他
    const getter = typeof userDef === 'function' ? userDef : userDef.get
      // 创建watcher,第二个参数是上方的getter方法,变换执行此方法
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions )
      // 代理到实例上
      defineComputed(vm, key, userDef)
    
  }
}

可以看到在initComputed函数中首先在实例vm上参见一个空对象_computedWatchers用来保存对象的watcher实例。然后对computed进行遍历获取每一项,然后对每一项的类型进行判断如果是函数就不变动是对象就获取器get函数。获取到后就new Watcher;之后进行代理。

下面先来看new Watcher中的操作;源码在src\core\observer\watcher.js(只显示本次有用的代码)

export default class Watcher {
  constructor (vm,expOrFn,cb,options,isRenderWatcher) {
    this.vm = vm
    // options
    if (options) {
      this.lazy = !!options.lazy
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {
    //代码省略
    return value
  }
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
​
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
​
  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
​

可以看到当我们此次new Watcher时只会执行constructor函数,因为我们在new Watcher时传的了computedWatcherOptions = { lazy: true }参数所以this.lazy的值为true,因此不会执行get函数,就不会将watcher保存在dep

接下来就会执行defineComputed函数对computed中的每一项进行代理是我们页面上能直接访问。源码在src\core\instance\state.js中以下代码是在源码的基础是哪个进行了简化处理的。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function defineComputed(target,key,userDef) {
  //代码进行了简化处理只考虑设置get
    sharedPropertyDefinition.get= createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

代码对computed中的每一项key进行了代理,获取就会执行对应的createComputedGetter函数。到此初始化过程就完成了

接下来是渲染过程。

在页面渲染时会获取computed中的数据,就会执行其对应的createComputedGetter函数代码如下

// 当组件中有值修改就会刷新页面就会读取Computed的值就会执行此函数,
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key] //获取对应的watch
    if (watcher) {  //当存在时,
      if (watcher.dirty) { //如果watcher.dirty为true调用watcher.evaluate()刷新watcher.value,如果为false就返回原始值
        watcher.evaluate()
      }
      // 将读取计算属性的那个watch添加到计算属性所依赖的所有状态列表中
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

拿到对应的watcher实例,因为我们默认watcher.dirtytrue就会执行watcher.evaluate()函数在上方Watcher类中。会执行get函数并且将dirty变为false。当执行get函数时会执行computed中对应的设置的函数,并且如果其中如果有vm上的数据就会执行其对应的get函数,就会将当前watcher实例保存在其对应的dep上。然后执行watcher.depend()函数,将保存了当前watcher实例的dep也保存在当前watcher实例中,即双向绑定为了解绑时在当前watcher实例中能找到dep,在dep上删除当前watcher实例。

当再次获取computed中的数据时(vm实例上的数据都没改变)因为watcher.dirty已经为false所有就会直接返回watcher.value

vm实例上的数据(data中的数据等等)发生改变时(computed中的数据使用过)因为都被代理拦截所有会执行对应的get函数,在给函数中会执行对应的dep.notify()会找到保存的watcher实例然后执行watcher实例的update 函数会将watcher.dirty变为true。下次获取computed中对应的数据就会执行watcher.evaluate()重复上方渲染时的操作。