computed-源码解析

在 Vue 3 中,computed 是响应式系统的核心模块之一,用于基于响应式数据派生出新的状态。其设计结合了依赖收集缓存机制异步调度,确保高效且精准的计算。以下从源码角度深入解析其实现原理。

一、核心类:ComputedRefImpl

computed 的核心逻辑封装在 ComputedRefImpl 类中,该类位于 @vue/reactivity 包的 computed.ts 文件。其核心结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ComputedRefImpl<T> {
public dep: Dep; // 依赖收集器
public effect: ReactiveEffect<T>; // 内部副作用函数
private _dirty = true; // 是否需要重新计算
private _value!: T; // 缓存计算结果
public __v_isRef = true; // 标记为 ref

constructor(
getter: () => T,
private readonly setter: (v: T) => void
) {
this.effect = new ReactiveEffect(getter, () => {
// 依赖变化时触发调度
if (!this._dirty) {
this._dirty = true;
trigger(this, 'set', 'value'); // 触发依赖更新
}
});
this.effect.lazy = true; // 延迟执行,首次访问才计算
}

get value() {
// 依赖收集:将当前 effect 与 computed 关联
track(this, 'get', 'value');
if (this._dirty) {
this._value = this.effect.run(); // 执行 getter
this._dirty = false; // 重置脏标记
}
return this._value;
}

set value(newValue: T) {
this.setter(newValue); // 执行用户定义的 setter
}
}

二、关键机制解析

1. 依赖收集与触发

  • 依赖收集:当访问 computed.value 时,track 函数会将当前 effect(通常是组件的渲染 effect)与 computed 实例关联。例如:
    1
    2
    3
    // 组件渲染时访问 computed
    const count = computed(() => state.a + state.b);
    // 渲染 effect 会被收集到 count.dep 中
  • 触发更新:当依赖的响应式数据(如 state.a)变化时,trigger 函数会遍历 computed.dep 中的所有 effect,并调用它们的 scheduler
    1
    2
    3
    // 依赖变化时触发
    state.a = 2;
    // computed 的 scheduler 会将 _dirty 设为 true

2. 缓存机制:脏标记(Dirty Flag)

  • _dirty 的作用computed 通过 _dirty 标志控制是否重新计算。首次访问时 _dirtytrue,执行 effect.run() 计算值并缓存到 _value,同时将 _dirty 设为 false。后续访问若 _dirtyfalse,直接返回缓存值。
  • 性能优化:只有当依赖变化时,scheduler 才会将 _dirty 设为 true,避免不必要的计算。例如:
    1
    2
    3
    // 依赖未变化时多次访问
    console.log(count.value); // 直接返回缓存值
    console.log(count.value); // 同上

3. 副作用调度

computedeffect 会在依赖变化时通过 scheduler 触发更新,而非立即执行:

1
2
3
4
5
this.effect = new ReactiveEffect(getter, () => {
// 调度逻辑:标记为脏数据,等待下次访问时重新计算
this._dirty = true;
trigger(this, 'set', 'value');
});

这种设计确保计算属性的更新与组件渲染同步,避免多次无效计算。

三、computed 函数的初始化

computed 函数负责创建 ComputedRefImpl 实例,并处理用户传入的 gettersetter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
): ComputedRef<T> {
let getter: ComputedGetter<T>;
let setter: ComputedSetter<T>;

if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = __DEV__
? () => warn('computed value is readonly')
: NOOP;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}

return new ComputedRefImpl(getter, setter);
}
  • 只读模式:传入函数时,setter 在开发环境抛出警告。
  • 可写模式:传入对象时,使用用户定义的 setter 处理赋值。

四、与 watch 的核心差异

特性 computed watch
依赖处理 基于依赖自动收集,缓存结果 显式监听数据源,无缓存
执行时机 首次访问时计算,依赖变化后重新计算 默认立即执行,后续依赖变化时执行
使用场景 同步计算派生值 处理副作用(如 API 调用、日志)
源码实现 基于 ComputedRefImplReactiveEffect 基于 watchEffecteffect

五、典型场景与源码关联

1. 基础使用

1
2
const count = ref(1);
const double = computed(() => count.value * 2);
  • 依赖收集:访问 count.value 时,double.effect 会被收集到 count.dep 中。
  • 更新触发count.value++ 时,count.dep 触发 double.effectscheduler,标记 double._dirty = true
  • 缓存机制:再次访问 double.value 时,若 _dirtytrue,重新计算并更新缓存。

2. 可写计算属性

1
2
3
4
5
const count = ref(1);
const double = computed({
get: () => count.value * 2,
set: (val) => count.value = val / 2,
});
  • setter 执行double.value = 4 会调用用户定义的 set,更新 count.value 为 2。
  • 依赖更新count.value 变化会触发 double 的重新计算。

六、性能优化与设计权衡

  1. 延迟计算effect.lazy = true 确保只有在访问 computed.value 时才执行计算,避免初始化时的不必要开销。
  2. 异步调度:依赖变化时通过 scheduler 触发更新,而非立即执行,与组件渲染周期同步。
  3. 多层依赖优化:若 computedA 依赖 computedBcomputedB 的更新会触发 computedA 的重新计算,但仅在必要时执行。

七、常见问题与源码解答

  1. 为何 computed 不会立即执行?
    因为 effect.lazy = true,首次访问 value 时才会调用 effect.run()

  2. 如何处理循环依赖?
    Vue 的响应式系统通过 activeEffect 栈和 allowRecurse 标志避免循环依赖导致的无限递归。

  3. 可写 computed 的性能影响?
    可写模式下,setter 的执行会触发依赖更新,但与只读模式相比,性能差异可忽略不计。

总结

computed 的源码设计充分体现了 Vue 3 响应式系统的核心思想:按需计算、精准依赖追踪、高效更新。其通过 ComputedRefImpl 封装计算逻辑,结合 ReactiveEffectDep 实现依赖管理,最终在性能和易用性之间取得平衡。理解这些机制有助于更合理地使用 computed,并深入排查响应式系统的潜在问题。