恋の歌的logo
Auto
归档 标签

Vue3.0Reactivity之computed

发表于2020-06-12 11:33
更新于2023-02-13 18:28
分类于编程
总字数2.6K
阅读时长 ≈10 分钟

前言 🔗

这次讲讲computed这个API

computed 🔗

computed在Vue2也有体现,基本上是一个缓存比较大的计算量的值的一种方法,并且能在依赖的值发生变化的时候自动的计算更新后的值。

在我们前面的翻译API的文章中知道了Vue3中computed接收一个getter函数,返回了一个ref的对象。

可以用一个例子来简单的看下Vue3中computed的使用。

typescript
const {reactive, effect, computed} = VueReactivity;

const o = {
  name: "lwf"
};

const r = reactive(o);

const c = computed(() => r.name + " --- computed");

effect(() => {
  console.log(r.name);
});

这时候我们尝试改变r.name的值,然后再输出c.value的值

在更新了响应式对象rname属性之后,它的依赖,也就是我们写的effect便执行了,然后再打印c.value,发现它的值已经发生了改变。

接下来,我们就来探究这个API是如何做到这种效果的。

首先,老套路,找到它的定义

typescript
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
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

这个函数很长,但总体上可以分成三个部分

  • 匹配参数
  • 建立依赖
  • 返回构建的computed对象(也就是ref对象)

匹配参数 🔗

typescript
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
  }
  // ...
}

这里判断了传进来的参数是不是一个函数,如果是,那么这个函数就作为一个getter(此时就是只读的),如果不是,那就分别取getset属性做为getter和setter。

一般而言,使用computed时,都是直接传递一个函数进去,也就是上面我举的例子一样,基本上用不到setter。

建立effect(依赖) 🔗

typescript
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  // ...
  let dirty = true
  let value: T
  let computed: ComputedRef<T>
  const runner = effect(getter, {
    lazy: true,
    // mark effect as computed so that it gets priority during trigger
    computed: true,
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  // ...
}

这里构建了一个effect,把传进来的getter传进了effect这个API,通过之前reactiveAPI的学习,我们知道,

effect传进一个函数,把这个函数和这个函数内部使用的响应式变量建立关联,当这些响应式变量发生改变时,就重新执行这个传进来的函数。

但是这个构建的effect和我们之前似乎有点不同,额外的传入了一个参数。

首先是lazy,这个参数的意思是,不马上执行(也就是自动)这个函数来建立依赖,而是通过返回的函数来手动的来收集依赖。

这里的lazyeffect的源码中非常简单

typescript
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  // 下面为lazy参数
  if (!options.lazy) {
    effect()
  }
  return effect
}

可以简单的理解,lazy为假,就自动帮你执行了这个在函数内部创建出来的effect,也就是自动的收集依赖。如果为真,直接返回,可以由用户自己执行来改变收集依赖的时机。

第二个参数为computed,这个参数上面有一个注释,翻译过来为:给这个effect添加一个标记,以至于它可以在trigger时,优先的执行。

第三个参数为scheduler,传入的为一个函数。

这里我们可以回到前面看trigger函数的实现。

typescript
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // ...
  const effects = new Set<ReactiveEffect>()
  // 这是一个计算属性的effect的Set
  const computedRunners = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          // 根据computed这个参数来分类effect
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
  }
  
  // ...
  
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // 如果scheduler有值,就执行scheduler,否则就执行effect。
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // 重点:计算effect需要提前运行。这样可以使得计算effect的getter的值在任何一般的effect运行之前失效
  computedRunners.forEach(run)
  effects.forEach(run)
}

具体的重点都写在代码的注释中了,简单讲就是分effect为两类,一类是一般effect,一类是计算effect,当一个响应式对象发生变化时,先执行它的计算effect,再执行它的一般effect。

为啥要先执行计算effect呢?这个我们最后再说。

返回构建的computed对象(也就是ref对象) 🔗

我们再看最后一段代码

typescript
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  // ...
  computed = {
    _isRef: true,
    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

最后就是构建一个ref对象,和一般的ref对象不同的是,计算ref有一个effect的属性,暴露了通过参数生成的effect对象,可以让我们停止这个effect。

这个构建的ref对象和我们之前通过ref构建的ref对象基本很像,但是在getter中出现了一个判断。判断了dirty的值来执行runner也就是effect。

那这个dirty是干什么用的呢?我们可以用开头举的例子。

typescript
const {reactive, effect, computed} = VueReactivity;
const o = {
  name: "lwf"
};

const r = reactive(o);
const c = computed(() => r.name + " --- computed");

effect(() => {
  console.log(r.name);
});

当我们打印c.value时,这是触发了value的getter,也就是到了

typescript
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  // ...
  computed = {
    // ...
    get value() {
      // 此时运行到这里
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    // ...
  } as any
  // ...
}

接下来我们判断了dirtydirty初始化是true的,所以会执行if块。

这时候执行了runner,也就是响应式对象r会收集到传入computed的函数。

runner的返回值也就是传入函数的返回值,赋给了value变量,然后置dirty变量为false,接着便是收集依赖,返回value变量了。

到这,我们就获得了computed对象的value属性了

如果现在我们再一次的运行c.value,依然会调用这个属性的getter,但是由于dirty变量为false,直接返回了value对象。

聪明的人可能明白了,dirty是用来判断依赖到的响应式对象是否发生改变的。

没改变的话,直接返回缓存的值,也就避免了多次的计算。

ok,这时,我执行了r.name = &#39;index&#39;,也就是会触发响应式对象r的所有依赖effect执行。

这时,响应式对象r的依赖中是有我们传入computed的函数所构成的effect的。

trigger函数中,有一个run函数

typescript
export function trigger(
  // ...
) {
  // ...
  
  const run = (effect: ReactiveEffect) => {
    // ...
    // 如果scheduler有值,就执行scheduler,否则就执行effect。
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  // 重点:计算effect需要提前运行。这样可以使得计算effect的getter的值在任何一般的effect运行之前失效
  computedRunners.forEach(run)
  effects.forEach(run)
}

这个run函数就是执行我们的effect的,其中判断了effect.options.scheduler,为真就传入effect对象并执行,也就是effect.options.scheduler(effect),为假就直接执行effect对象。

还记得我们之前通过传入函数生成一个计算effect时传入的额外参数吗?

typescript
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  const runner = effect(getter, {
    // ...
    scheduler: () => {
      //这里就是当计算effect依赖的响应式对象发生改变时,会执行的代码段。
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
}

这里判断了dirty变量,如果为false,就置为true并触发这个计算ref的依赖,dirty为假的意思就是此时计算ref所依赖的响应式对象已经发生改变了。

前面我们已经打印了c.value的值了,这时的dirty就是false了,进入了if的代码块。置为dirtytrue,触发了计算ref的依赖。

如果一直没有取value属性的值,那么就没有必要去触发计算ref的依赖。在第一次获取计算ref的值(.value)之前,它的值都是不确定的(也不能说不确定,就是会根据它所依赖响应式对象的最新值来进行计算并返回)。

这时我们再次打印c.value,由于响应式对象r的改变使得dirty变为了true,就又要再执行一次runner来获取最新的值。

最后,为什么要先执行响应式对象的计算effect呢?

很简单,我们用下面的代码来理解

typescript
const {reactive, effect, computed} = VueReactivity;

const o = {
  name: "lwf"
};

const r = reactive(o);

const c = computed(() => r.name + " --- computed");

effect(() => {
  console.log(r.name);
  console.log(c.value);
});

这时运行r.name = &#39;index&#39;的话,这时会打印两次effect

text
// r对象改变触发了effect
'index'
'index --- computed'
// c对象改变触发了effect
'index'
'index --- computed'

如果你没有先执行计算effect的话,此时对于该计算ref的dirty还是false,也就是没有通知到计算ref对象此时依赖的响应式对象发生了变化,使用到了旧的值。也就是打印了

text
// r对象改变触发了effect
'index'
// c的getter中dirty还是为false,使用到了旧的值。
'lwf --- computed'
// c对象改变触发了effect
'index'
'index --- computed'

至此,computedAPI的基本流程基本上就讲完了。不得不说实现上确实很巧妙,用到了很多的闭包。

后记 🔗

还差最后一个readonlyAPI了,这个和reactive差不多,这几天应该就可以写出来了,如果我写的有错,希望可以指出,非常感谢!~

#JavaScript
#Vue
哦呐该,如果没有评论的话,瓦达西...