计算属性大家肯定都很熟悉了

其内部实现依赖 effect,追踪到变化后重新计算结果,并且返回的是一个类似 ref 的玩意

其源码在 packages/reactivity/src/computed.ts

computed

我们直接定位到方法,可以看到有好几种声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 只传 getter
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
// 传一个对象,里面放有 getter 和 setter
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
// 实现
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
...
}

分别表示只传 gettergetter,setter 都传的情况

来看实现:

  • 定义 gettersetter

    1
    2
    let getter: ComputedGetter<T>;
    let setter: ComputedSetter<T>;
  • 分别处理只传了 getter 和都传的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const onlyGetter = isFunction(getterOrOptions);
    if (onlyGetter) {
    getter = getterOrOptions;
    setter = __DEV__
    ? () => {
    console.warn('Write operation failed: computed value is readonly');
    }
    : NOOP;
    } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
    }

    gettersetter 赋值

  • 构建一个 ComputedRefImpl 的实例并返回:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const cRef = new ComputedRefImpl(
    getter,
    setter,
    onlyGetter || !setter,
    isSSR
    );

    if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack;
    cRef.effect.onTrigger = debugOptions.onTrigger;
    }

    return cRef as any;

ComputedRefImpl

是计算属性的核心实现

成员变量

  • dep : 依赖当前计算属性的副作用集合

  • _value : 计算的结果

  • effect : 用于捕获 getter 方法中依赖的响应式值的副作用

  • __v_isRef : 标识该对象为 ref

  • [ReactiveFlags.IS_READONLY] : 标识该对象是否为只读的

  • _dirty : 用于标识当前计算结果是否已经不可用(依赖的响应式值变动了,但是还没计算)

  • _cacheable : 不为 SSR 环境时为 true,我们只讨论非 SSR 的实现

构造器

接收四个参数:

  • getter : 计算方法

  • _setter : 赋值方法

  • isReadonly : 是否只读,computedsetter 的情况下才为 true

  • isSSR : 是否为服务端渲染,只讨论 false 的情况

1
2
3
4
5
6
7
8
9
10
11
12
export class ComputedRefImpl<T> {
...
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
...
}
...
}

来看实现

  • 首先将 getter 方法作为回调构建一个副作用

    1
    2
    3
    4
    5
    6
    this.effect = new ReactiveEffect(getter, () => {
    if (!this._dirty) {
    this._dirty = true;
    triggerRefValue(this);
    }
    });

    同时传入了一个适配器方法,getter 中的响应式对象属性变化并调用 trigger 时,此时会优先调用适配器(第二个参数传的函数)

    适配器逻辑很简单

    • 如果当前计算结果标记为干净的

    • 则将其设为脏

    • 并触发依赖当前计算结果(computed.value)的副作用

  • 设置各种 Flag

    标记当前副作用为 computed 类型

    1
    this.effect.computed = this;

    标记当前副作用为活跃的,当前计算属性是可缓存的(只讨论 isSSRfalse 的情况)

    1
    this.effect.active = this._cacheable = !isSSR;

    设置只读标记

    1
    this[ReactiveFlags.IS_READONLY] = isReadonly;

value

  • getter

    用于拦截 value 属性的读取行为

    1
    2
    3
    4
    5
    6
    7
    export class ComputedRefImpl<T> {
    ...
    get value() {
    ...
    }
    ...
    }

    来详细看下 get value() { ... } 的实现

    • 可能被其它方法套娃(readonly等),首先确保拿到的对象本身

      1
      2
      // the computed ref may get wrapped by other proxies e.g. readonly() #3376
      const self = toRaw(this);
    • 追踪各种副作用和依赖

      先调用 trackRefValue 追踪依赖 value 的副作用

      1
      trackRefValue(self);

      接着如果当前值为脏,则调用副作用的 run 来重新运行 getter 计算结果,将结果赋值给 _value

      1
      2
      3
      4
      if (self._dirty || !self._cacheable) {
      self._dirty = false;
      self._value = self.effect.run()!;
      }

      最后返回计算结果 _value

      1
      return self._value;
  • setter

    就很简单,直接调用当前 _setter

    1
    2
    3
    4
    5
    6
    export class ComputedRefImpl<T> {
    ...
    set value(newValue: T) {
    this._setter(newValue);
    }
    }

计算流程

例如下面代码:

1
2
3
4
5
6
7
const countA = reactive({ count: 1 });
const countB = reactive({ count: 1 });
const c = computed(() => countA.count + countB.count);

const e = effect(() => console.log(c.value));

countA.count++;
  1. 调用 computed 构建计算属性对象 c

  2. 声明副作用 e

  3. e 的回调会立刻执行,回调读取了计算属性的值 c.value

  4. 计算属性立刻计算出值并返回,此时响应式系统也得知副作用 e 依赖 c.value

  5. 回调执行完毕

  6. countA.count++ 修改了响应式对象的值,触发了计算属性 c 内部的副作用 c.effect

  7. c.effect 触发后会立刻触发了依赖 c.value 的副作用,副作用 e 被触发,执行 3~5