Vue3.0源码解析「reactive」篇 — 4.computed

481 阅读3分钟

computed

其实我觉得 computed 就是 effect 方法和 ref 的结合,effect 允许传入一个表达式,并且将其视为副作用进行跟踪。而 ref 则通过 value 属性的拦截器构造一个响应式结构,下面是一个 computed 的例子:

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接受一个 getter 或者一个 setter&getterOption 作为参数返回一个默认不可手动修改的 ref 对象,这个入口函数主要做的就是解析出参数中的 gettersetter 然后构造一个 ComputedRefImpl

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
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

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

下面是 Gettersetter 的类型定义:

export type ComputedGetter<T> = (ctx?: any) => T
export type ComputedSetter<T> = (v: T) => void

export interface WritableComputedOptions<T> {
  get: ComputedGetter<T>
  set: ComputedSetter<T>
}

最后的返回值也没什么好说的,两个属性在 ComputedRefImpl 中都能找到:

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
  readonly value: T
}

export interface WritableComputedRef<T> extends Ref<T> {
  readonly effect: ReactiveEffect<T>
}

ComputedRefImpl

ComputedRefImpl 就是 computed 的内部实现啦,和 RefImpl 类似通过 _value 属性缓存值,只不过这个值是由包裹在 effect 中的 getter 计算而来的。

先来看看属性:

  • _value:储存计算值;
  • _dirty:完成计算标记位,计算完成为 false,有 reactive 值改变的时候会变为 false
  • effect:缓存延迟执行的 effect(effect)包裹着我们传入的 setter 作为执行函数;
  • __v_isRefref 标志位,表示这个结构是可以解构的,有和 ref 相同的特性;
  • IS_READONLY:只读标志位,当我们只传递一个 getter 参数的时候这个属性为 true
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
}

然后我们先看看 setter,发现只是调用了一下传入的 setter,这个时候你肯定有疑问,set 操作的时候不是应该调用 trigger 函数触发引用这个 computed 的副作用吗?

答:这是因为 setter 未必导致 computed计算值改变,因为 computed 是依赖其他响应式对象计算的,只有这些对象改变的时候 computed 计算结果才会改变。

class ComputedRefImpl<T> {
  set value(newValue: T) {
    this._setter(newValue)
  }
}

image-20210815203310954.png

其实 computed 的响应式实现还是挺复杂的,如上图是一个比较完整的更新流程:

  1. step1:构造 computed state 内部引用了 reactive obj,此时将 getter 作为参数构造构造了一个 effect,这个 effect 有惰性标记不会被立刻执行;
  2. step2:声明副作用函数 effect内部引用 state.value这个 computed ref
  3. step3state.value 触发了 computed getter,由下面的代码我们知道第一次执行(dirty)会调用 this.effect
    1. this.effect 执行之前的 getter 参数,并且触发 obj.numgetter,现在 obj.num trackthis.effect
    2. this.effect 的计算结果赋值给 _value 并且返回,更新 dirty 标记位;
    3. 最后 computed statetrackstep2 执行的 effect
  4. step4:执行一个 set 操作,set 操作中的 obj.num=num 出发了 obj.num 的 setter 代理并 trigger this.effect;
    1. 由于 this.effectscheduler,所以不会执行 getter 而是会执行 scheduler,设置 dirty 标记位,并且 trigger computed deps
    2. computed deps 包含 step2effect,重新执行 numpy = state.value
    3. state.value 又触发了 computed getter,重新计算值,更新脏标记位,这套流程才走完。

现在我们就知道为什么 trigger 不发生在 setter 里面了,因为只有 computed 内部引用的 reactive 改变了才应该 trigger,但 computed setter 内未必对引用的 reactive 造成 setter

而引用的 reactive 如果触发 setter(不管是不是在 setter)内,都会触发 this.effectthis.effect 则会自动触发 trigger

class ComputedRefImpl<T> {
  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)
  }
}