Vue3 之 computed 计算属性的使用与源码分析详细注释

发布时间 2023-07-06 15:35:31作者: Echoyya、

计算属性的基本用法

computed 一般有两种常见的用法:
一:传入一个对象,内部有 set 和 get 方法,属于ComputedOptions形式。在内部会有getter / setter两个变量来进行保存.

const age = ref(18);
const myAge = computed({
  get() {},
  set() {},
});

二:传入一个 function,在内部会有getter来进行保存.

const age = ref(18);
const myAge = computed(() => {
  return age.value + 10;
});

计算属性的源码

计算属性的源码大部分是依赖 effect 的实现。基于上一篇文章对 effect 源码的理解,effect 可以传递一个函数和一个对象 options。
而计算属性的本质就是一个 effect,在之前 effect 的源码中预先声明了 lazyscheduler 属性,就是用于计算属性,因为计算属性默认不会被执行,
lazy 表示 effect 不会立即被执行,scheduler 会在 trigger 中判断是否传入了 scheduler,传入就执行 scheduler 方法。
scheduler 中,判断当前的_dirty 是否为 false,会把_dirty 置为 true,且执行 trigger 触发响应。

computed 也是基于类实现 ComputedRefImpl
其中 share.ts、effect.ts 文件中的方法,不在赘述,详见上一篇 响应式原理的文章

// computed.ts
import { isFunction, TrackOpTypes, TriggerOrTypes } from "./shared";
import { effect, track, trigger } from "./effect";

class ComputedRefImpl {
  public _dirty = true; // 默认取值时不要用缓存
  public _value;
  public effect;
  constructor(getter, public setter) {
    this.effect = effect(getter, {
      // 1. 计算属性本身就是一个effect
      lazy: true, // 2. 默认不执行
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true;
          trigger(this, TriggerOrTypes.SET, "value");
        }
      },
    });
  }
  get value() {
    // 3.计算属性也要收集依赖,vue2中计算属性不具备收集依赖的,
    if (this._dirty) {
      this._value = this.effect(); // 4. 会将用户的返回值返回 也就是computed中 return
      this._dirty = false; // 5. 设置缓存
    }
    track(this, TrackOpTypes.GET, "value");  // 6. 依赖收集: 因为可能会在effect中使用计算属性
    /**
     *  const age = ref(18)
        const myAge = computed(() => { // 此方法默认不会被执行
            return age.value + 10;
        })

        // 当访问属性的时候执行
        console.log(myAge.value)
        console.log(myAge.value) // 多次取值,只取第一次执行结果走缓存

        age.value = 100; // 更新age,myAge不会立刻重新计算
        console.log(myAge.value) // 取值时才会重新计算

        effect(() => {  // 此effect中没有age 但是用到了计算属性,因此也需要依赖收集
            console.log(myAge.value)
        })
        age.value = 500  // 收集依赖后,属性值更新,需要在scheduler中触发trigger执行,
     */
    return this._value;   // 7. 多次取值,只取第一次执行结果走缓存
  }
  set value(newValue) {
    this.setter(newValue);
  }
}

// 1. 如果getterOrOptions是函数,会直接赋值给getter。并在用户进行复制操作时 给出只读提示
// 2. 否则getterOrOptions为对象,会将set和get分别赋值给setter,getter。
export function computed(getterOrOptions) {
  let getter;
  let setter;
  // 1. 如果getterOrOptions是函数,会直接赋值给getter。并在用户进行复制操作时 给出只读提示
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = () => {
      console.warn("computed value must be readonly");
    };
  } else {
    // 2. 否则getterOrOptions为对象,会将set和get分别赋值给setter,getter。
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  return new ComputedRefImpl(getter, setter);  // 3. 创建一个计算属性实例
}

shared 工具方法抽离

// share.ts
export const isFunction = (value) => typeof value == "function";