Vue3源码学习——计算属性computed

248 阅读5分钟

前面几节,我们已经学习了很多响应式相关的源码,有点遗忘的朋友可以再回顾一下。

这一节我们继续探究Vue3的源码,来看一下计算属性computed是如何实现的。

computed

基本用法

我们先看一下computed的基本用法:

定义:接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

举例

const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误 只含有getter的计算属性,不能修改
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 带set的计算属性,可修改

computed 的使用还是比较简单的,但是我们知道 计算属性 的优点在于它具有 缓存性,不会进行一些无用的重复计算,只有当它 依赖属性 发生变化的时候才会重新计算,这样也就能提高Vue整体的性能。接下来,我们正式从源码层看看它是如何实现的:

computed

function computed(getterOrOptions, debugOptions) {
    let getter;
    let setter;
    // 判断传入的第一个参数是否为函数,如果是函数形式,则说明只有一个getter
    const onlyGetter = isFunction(getterOrOptions);
    if (onlyGetter) {
      getter = getterOrOptions;
      // 当计算属性没有传入set时,将setter定义为报警函数
      setter = () => {
        console.warn("Write operation failed: computed value is readonly");
      } 
    } else {
      getter = getterOrOptions.get;
      setter = getterOrOptions.set;
    }
    const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter);
    return cRef;
  }

computed这个API做的事情,就是根据接到的参数定义 gettersetter,如果没有传入 setter,则将setter定义为一个在控制台打印报警信息的函数,最后创建一个 ComputedRefImpl实例,并返回出去。

根据 computed 的定义,我们知道最后返回出去的是一个 ref对象,那么 ComputedRefImpl 是如何创建这个 ref对象 的呢?我们接着往下看:

ComputedRefImpl

var ComputedRefImpl = class {
    constructor(getter, _setter, isReadonly) {
      // 定义setter方法
      this._setter = _setter;
      this.dep = void 0;
      // 是否为ref类型
      this.__v_isRef = true;
      // 是否只读
      this.__v_isReadonly = false;
      // 用于控制是否走缓存值
      this._dirty = true;
      // 将getter包装为一个副作用函数effect
      this.effect = new ReactiveEffect(getter, () => {
        if (!this._dirty) {
          this._dirty = true;
          // 触发更新
          triggerRefValue(this);
        }
      });
      // 为副作用函数赋computed属性,在触发阶段会优先执行带有computed属性的effect
      this.effect.computed = this;
      this.effect.active = this._cacheable = true;
      this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
    }
    get value() {
      const self = toRaw(this);
      // 依赖收集
      trackRefValue(self);
      // 如果是SSR模式,则_cacheable则默认为false,从而每次get都会重新计算
      if (self._dirty || !self._cacheable) {
        self._dirty = false;
        // 执行副作用函数获取执行结果
        self._value = self.effect.run();
      }
      return self._value;
    }
    // 修改计算属性
    set value(newValue) {
      this._setter(newValue);
    }
  };

上面就是 ComputedRefImpl 的整体逻辑,我们来一步步分析:

  • 首先,会将传入的修改方法 setter 赋值给自身 _setter属性,确定了实例的 修改方法set。当计算属性发生修改操作时,直接触发 _setter 的执行。
  • 然后,将 getter函数 包装为 副作用函数effect ,当 getter函数 中的 依赖 发生变化时,会触发 scheduler调度 参数中的函数执行,从而将 _dirty 赋值为 true,并触发更新。
  • 最后,为实例确定 get方法,当我们尝试获取计算属性的值时,会触发get方法的执行,首先会对计算属性进行 依赖收集,然后根据 _dirty值判断是否需要重新计算。如果 _dirtytrue,则重新运行副作用函数effect 获取最新值并赋值给 _value,并返回出去;如果为 false,则直接返回缓存值

额外说明

计算属性的缓存是通过_dirty 和 _cacheable这两个属性来控制的。 _dirty用于控制计算属性的取值是否重新计算,而当我们采用的是SSR模式的话,则会导致 _cacheable属性为true,则每次取值都会涉及重新计算

整个流程大概如下图:

image.png

至此,关于计算属性computed的缓存功能的实现我们大概就了解了。

triggerEffects

再补充一个小的点:在源码中,我们有注意到,computed中在将getter包装为副作用函数effect之后,还为effect赋上了computed属性,这个属性有什么用呢?

这里就要再提一下触发副作用执行的函数:triggerEffects

// 先执行带有computed属性的副作用函数,再执行普通的副作用函数
function triggerEffects(dep, debuggerEventExtraInfo) {
    const effects = isArray(dep) ? dep : [...dep];
    for (const effect of effects) {
      // 如果副作用函数上有computed属性,则优先执行带有computed属性的副作用函数
      if (effect.computed) {
        triggerEffect(effect, debuggerEventExtraInfo);
      }
    }
    for (const effect of effects) {
      if (!effect.computed) {
        triggerEffect(effect, debuggerEventExtraInfo);
      }
    }
}

当同一个依赖发生改变会触发多个副作用函数时,会优先执行带有computed属性的副作用函数。

总结

最后,关于这一节我们学习的计算属性computed做一个总结:

  • 首先,computed会根据传入的参数的类型决定该计算属性是否可修改,如果没有传入set方法,则将修改方法setter定义为报警函数
  • 然后,将getter函数包装为一个副作用函数effect,当getter的依赖项发生改变时,会触发scheduler函数的执行,将_dirty赋值为true,同时触发更新操作。
  • 最后,当我们去获取计算属性 computed的值时,会根据 _dirty 来决定是否要重新计算:
    • 当依赖项发生改变的时候,触发sheduler调度执行,_dirty=true操作,当获取computed的值时就会重新计算
    • 依赖项没有发生改变,则不会触发sheduler调度执行,那么_dirty值为false,计算属性直接返回缓存值

参考文章:

官网

大佬的小册