【vue】computed的原理?

665 阅读3分钟

1、关于computed的一些问题

  1. computed属性是什么时候初始化的?
  2. 为什么不能在data中使用computed
  3. 怎样设置computed不缓存结果, 就算依赖未发生变化也重新计算?
  4. 假设计算属性b依赖组件数据data.a, 那么当data.a改变时是怎么通知到使用了计算属性b的watcher的?

2、computed的初始化

computed的初始化是在this._init里完成的。

image.png

this._init里面调用了initState方法。从下面的代码可知data是在computed之前进行初始化的,所以是访问不到computed属性。那如果执意要访问呢? image.png

initState内部调用initComputedcomputed初始化。

3、initComputedcom

initComputed的大概逻辑是:

  1. 遍历我们定义好的computed对象,也就是options中的computed对象。每一个属性都创建一个对应的Watcher对象。Watcher对象的get方法就是我们定义的computed属性的getter。因为computedWatcherOptions设置了lazytrue,所以这里getter不会立即执行。
  2. defineComputed(vm, key, userDef)在组件实例上定义一个与computed的key同名的属性, 这就是为什么我们能直接通过this访问。
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  
    for (const key in computed) {
        const userDef = computed[key]
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        // ....
        
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          )
       
        
        // ...
        defineComputed(vm, key, userDef)
     }
}

4、defineComputed

  1. computed属性是通过Object.defineProperty(target, key, sharedPropertyDefinition)定义的访问器属性。
  2. 访问器属性的set方法就是用户定义computed时定义的set方法,如果没有定义则是一个noop函数。
  3. 访问器属性的get方法处理逻辑要复杂些。
    1. 如果定义的computed属性只是一个函数
      • 在浏览器时是createComputedGetter(key)创建get方法
      • 服务端是createGetterInvoker(userDef)创建get方法
    2. 如果定义的computed属性只是一个对象
      • 当定义computed时指定了cache: false, 使用createGetterInvoker(userDef)创建get方法。
      • 未指定cache: false, 使用createComputedGetter(key)创建get方法。
// userDef.cache可以控制是否缓存computed属性的值
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
      )
    }
  }
  // 组件实例访问key其实访问的是对应watcher的value
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

5、createComputedGetter

createComputedGetter创建的get会使用缓存。

  1. 在第3节我们已经知道了每个computed属性都会有一个对应的watcher,当第一访问computed属性时,watcher.dirty的值是true。因为我们创建Watcher的时候传入的lazytrueimage.png

所以会通过watcher.evaluate(),调用get计算computed的值, 计算得到的结果是缓存在watcher.value上的。 image.png

第一次计算完后watcher.dirty的值变成false,所以只要watcher.dirty不变成true,watcher.evaluate()就不会重复调用。

  1. 那么watcher.dirty的值什么时候会重新变成true呢?

image.png 当调用watcher.update的时候就会更新dirty的值为true。熟悉Vue响应式原理的朋友应该知道,当Observer对象的发生改变时就会通过Dep对象去执行Watcherupdate方法。所以当computed属性的依赖发生变化时,只是更改了dirty的值, 只有当下一次访问computed属性执行get方法时才会重新计算。

  1. computed本质上也是一个watcher对象,那么它是怎样被其它watcher对象订阅的呢?

image.png

上面的代码会遍历computedWathcer订阅的数据,使Dep.target也重新订阅一边。

// 会对computed属性进行缓存
function createComputedGetter (key) {
  return function computedGetter () {
    // 访问computed属性时,其实访问的时内部watcher的值
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 如果Watcher的值需要重新计算时
      // 在evaluate内部会执行用户定义的getter, 在getter内依赖的数据都会被watcher收集
      // computed手机依赖
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 如果Dep.target存在的话, 可能是renderWathcer 也可能是computedWatcher、userWatcher
      // 会将watcher收集的依赖同时被Dep.target收集
      // 举个例子: computed属性a依赖 $data.b
      // 在template中使用了a, 那么renderWatcher也会间接依赖$data.b
      // 当$data.b的值变化时,会将watcher.dirty的值变成true
      // 同时也会通知renderWatcher进行patch
      if (Dep.target) {
        // 将computed收集到的依赖给使用了这个computed属性的Watcher也弄一份
        // 所以当computed属性的依赖发生变化时,computedWatcher的dirty值变成true, 但式不会立马重新求值
        // 同时也会通知到使用了这个computed属性的watcher(比如renderWatcher),在执行renderWatcher时使用到这个computed属性时才会重新求值。
        watcher.depend()
      }
      // 返回watcher的值
      return watcher.value
    }
  }
}

7、createGetterInvoker

不使用watcher进行结果缓存。

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}