vue3.0的computed通过自身value来触发依赖更新

3,396 阅读2分钟

computed在vue3中的实现

使用方式:

computed(() => {console.log(state.a)})

核心代码:

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
}

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() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

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

由上可知

  1. 计算属性触发求值时,就会触发get value()
  2. 通过dirty开关,来实现当计算属性的值不发生改变时,不会执行if内的内容,直接返回value
  3. 所以当第一次求值时,通过this.effect()来求值 详细看看this.effect()
    const effect = function reactiveEffect(): unknown {
        if (!effect.active) {
          return options.scheduler ? undefined : fn()
        }
        // effectStack是这个栈应该是收集effect,
        if (!effectStack.includes(effect)) {
          // 当前effect的deps中去除effect
          cleanup(effect)
          try {
            enableTracking()
            effectStack.push(effect)
            activeEffect = effect
            return fn()
          } finally {
            effectStack.pop()
            resetTracking()
            activeEffect = effectStack[effectStack.length - 1]
          }
        }
      } as ReactiveEffect
    effect.options = options //options中有scheduler
    
    主要做了
    1. 将当前的依赖effect推入activeEffect中
    2. 执行fn(),也就是getter(),触发内部响应值来收集当前effect依赖,就是当前effect
    3. 最后finally,effect推出,acitveEffect为栈顶元素,这个值往往是渲染函数render
  4. 接着执行这个track(toRaw(this), TrackOpTypes.GET, 'value'),这个是实现计算属性来收集自己的value值的依赖,这时计算属性value就将渲染函数render收集到自己的依赖中
  5. 当内部元素的值发现改变时,触发依赖更新会执行下面的代码
const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // 给computed使用的 当computed中的值改变时执行下面
    //if (!this._dirty) {
    //           this._dirty = true
    //           trigger(toRaw(this), TriggerOpTypes.SET, 'value')
    //         }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  1. 因为计算属性的effect是有effect.options.scheduler的值的
this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })
  1. 所以执行上的scheduler()
  2. 当内部元素值方式改变时要重新求值,所以将dirty变成true,同时触发toRaw(this)的依赖,这个依赖就是渲染函数render,
    1. toRaw(this) 这个就是自己
    export function toRaw<T>(observed: T): T {
    // 这种写法替换if-else 厉害
      return (
        (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
      )
    }
    
  3. 因为在执行渲染函数时就一定会触发计算属性的求值,从而实现重新赋值以及重新收集依赖

总结

相对于vue2,之前是通过计算属性内部的依赖值来收集计算属性的watcher以及计算属性触发的渲染函数render的依赖;而vue3可以明显的看到是通过自身的value来收集自身触发的渲染函数render的effect,实现上更加巧妙和解耦