Vue3进阶之computed实现原理

5,078 阅读4分钟

什么是computed

computed 是组件的计算属性,它的含义是依赖于其他状态而生成的状态。

computed的使用方法

computed 有两种创建方式

  1. computed 函数传递一个 getter 方法创建 immutable reactive ref object
const count = ref(1)

const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value = 3 // error,因为plusOne是immutable ref obj
  1. computed 函数传递一个有 getset 方法的对象来创建一个 writable ref object
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

computed缓存值的原理

熟悉 Vue 的开发者都知道 computed 的特性就在于能够缓存计算的值(提升性能),只有当 computed 的依赖发生变化时才会重新计算,否则读取 computed 的值则一直是之前的值。那么 computed 是怎么做到这一点的呢?为什么 computed 能够知道依赖已经发生了改变需要重新计算呢?当创建一个 computed 是在 Vue 的内部到底发生了什么?让我们一起来看看吧。 ​

创建一个computed

创建 computed 需要调用 computed 函数,computed 函数接收一个 getter 方法或者是一个含有 get 方法和 set 方法的对象,并返回一个 ref 对象。

以下是 computed 工厂函数的全部代码:

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

产生ref对象

通过上面的代码我们发现,Vue 通过执行构造方法ComputedRefImpl来创建ref对象,哦,我们现在知道了computed 函数返回的ref对象其实就是执行构造方法 ComputedRefImpl 而创建的一个实例。那么能让computed 缓存值的功能一定在ComputedRefImpl 的内部逻辑里了,我们一起接着往下看。 ​

以下是 ComputedRefImpl 的全部代码:

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true
	
  public readonly effect: ReactiveEffect<T>
	
  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    track(self, TrackOpTypes.GET, 'value')
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

我们一起来看 ComputedRefImpl 的构造方法到底做了什么,该构造方法一共做了两件事:

  1. 调用 effect 方法生成 watcher 监听函数并赋值给实例的 effect 属性,(effect方法来自于reactivity/effect.ts)
  2. 设置ref对象是否为 readonly

不过构造方法好像和 computed 的功能扯不上关系,不要急,我们接着往下看。 ​

getter 方法的执行时机

我们发现声明一个 computed 时其实并不会执行getter方法,只有在读取 computed 值时才会执行它的 getter 方法,那么接下来我们就要关注 ComputedRefImplgetter 方法。 ​

getter 方法会在读取 computed 值的时候执行,而在 getter 方法中有一个叫 _dirty 的变量,它的意思是代表脏数据的开关,默认初始化时 _dirty 被设为 true ,在 getter 方法中表示开关打开,需要计算一遍computed 的值,然后关闭开关,之后再获取 computed 的值时由于 _dirtyfalse 就不会重新计算。这就是 computed 缓存值的实现原理。

computed重新计算值

那么 computed 是怎么知道要重新计算值的呢? 其实很简单就是在某个地方将 _dirty 的值设为 true ,获取 computed 的值的时候不就会重新计算了嘛。那么在哪里出现了将 _dirty 设为 true 的操作呢?没错,就是在构造函数里。effect 函数会给对象的响应式对象生成监听函数,并对 scheduler 进行了设置,还记得我们在创建 computed 时传递进来的 getter 方法吗,此方法就是 computed 内部依赖的状态变化时会执行的操作。所以最终的流程就是:computed 内部依赖的状态发生改变,执行对应的监听函数,这其中自然会执行 scheduler 里的操作。而在 scheduler 中将 _dirty 设为了 true

也许看到这里有人还会产生一个疑问,computed是怎么知道内部依赖产生了变化呢?这是由于在我们第一次获取computed值(即执行getter方法)的时候对内部依赖进行了访问,在那个时候就对其进行了依赖收集操作,所以computed能够知道内部依赖产生了变化。