vue3 computed解析

12 阅读5分钟

要理解 computed 的底层实现,核心要抓住两个关键点:响应式依赖追踪缓存机制。Vue3 的 computed 源码位于 packages/reactivity/src/computed.ts 文件中,以下从核心逻辑、关键类 / 函数、执行流程三个维度拆解。

一、核心前置知识

在看 computed 源码前,需先了解 Vue3 响应式的两个基础模块:

  1. Effect(副作用)computed 本质是一个特殊的 effecteffect 是响应式系统的核心,用于追踪依赖、触发更新;
  2. Ref/Track/Triggertrack 用于收集依赖,trigger 用于触发依赖更新,Ref 是响应式数据的基础封装。

二、computed 核心源码结构

先看 computed 函数的入口代码(简化核心逻辑):

// packages/reactivity/src/computed.ts
import { effect, ReactiveEffect } from './effect'
import { Ref, ref } from './ref'

// 计算属性的选项类型:支持 get/set(可写) 或仅 get(只读)
export type ComputedOptions<T> = {
  get: () => T
  set: (v: T) => void
}

// 核心入口函数
export function computed<T>(
  getterOrOptions: (() => T) | ComputedOptions<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T> {
  // 1. 区分传入的是 getter 函数 还是 get/set 配置对象
  let getter: () => T
  let setter: (v: T) => void

  const isReadonly = typeof getterOrOptions === 'function'
  if (isReadonly) {
    // 只读型:getter 就是传入的函数,setter 为空(赋值会报错)
    getter = getterOrOptions
    setter = () => {
      console.warn('Write operation failed: computed value is readonly')
    }
  } else {
    // 可写型:解构 get/set
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 2. 创建 ComputedRefImpl 实例(核心逻辑封装类)
  const cRef = new ComputedRefImpl(getter, setter, isReadonly)

  return cRef as ComputedRef<T>
}
核心类:ComputedRefImpl(计算属性的实现核心)

这个类是 computed 的灵魂,封装了缓存、依赖追踪、更新触发的全部逻辑:

class ComputedRefImpl<T> {
  // 缓存的计算结果
  private _value!: T
  // 标记:是否需要重新计算(依赖变化时置为 true)
  private _dirty = true
  // 核心:计算属性对应的 effect(副作用)
  public readonly effect: ReactiveEffect<T>
  // 实现 Ref 接口,标记为响应式对象
  public readonly __v_isRef = true
  // 标记是否只读
  public readonly __v_isReadonly: boolean

  constructor(
    getter: () => T,
    private readonly _setter: (v: T) => void,
    isReadonly: boolean
  ) {
    this.__v_isReadonly = isReadonly

    // 3. 创建 ReactiveEffect(特殊的 effect,lazy: true 表示不会立即执行)
    this.effect = new ReactiveEffect(getter, () => {
      // effect 的调度函数:依赖变化时触发
      if (!this._dirty) {
        this._dirty = true // 标记需要重新计算
        triggerRefValue(this) // 触发计算属性的依赖更新(通知使用该计算属性的地方)
      }
    })
    // 标记 effect 为 computed effect,用于优先级区分
    this.effect.computed = this
    this.effect.active = true
  }

  // 取值时的 get 拦截(访问 .value 时触发)
  get value() {
    // 4. 依赖追踪:收集当前组件/effect 对该计算属性的依赖
    trackRefValue(this)
    // 5. 缓存逻辑:_dirty 为 true 时才重新计算
    if (this._dirty) {
      this._dirty = false // 计算后标记为“干净”,下次直接用缓存
      // 执行 effect.run() 会调用 getter,得到最新值并缓存
      this._value = this.effect.run()!
    }
    return this._value
  }

  // 赋值时的 set 拦截(修改 .value 时触发)
  set value(newValue: T) {
    this._setter(newValue) // 执行用户定义的 setter
  }
}

三、computed 执行流程(核心逻辑拆解)

以 “只读型计算属性” 为例,完整执行流程如下:

1. 初始化阶段
const num = ref(10)
const doubleNum = computed(() => num.value * 2)
  • 调用 computed 函数,传入 getter 函数 () => num.value * 2

  • 初始化 ComputedRefImpl 实例,创建 ReactiveEffect(副作用):

    • lazy: true:effect 不会立即执行 getter;
    • 调度函数:当依赖(num)变化时,将 _dirty 置为 true,并触发 trigger 通知依赖该计算属性的地方。
2. 首次访问计算属性(doubleNum.value
console.log(doubleNum.value) // 20
  • 触发 ComputedRefImplget value() 方法;

  • 执行 trackRefValue(this):收集当前执行上下文对 doubleNum 的依赖(比如组件渲染 effect);

  • 此时 _dirtytrue,执行 effect.run()

    • 执行 getter 函数 () => num.value * 2
    • 访问 num.value 时,numtrack 会收集 doubleNum 的 effect 作为依赖;
    • 将 getter 返回值(20)赋值给 _value(缓存),并将 _dirty 置为 false
  • 返回缓存值 _value(20)。

3. 重复访问计算属性
console.log(doubleNum.value) // 20
  • 触发 get value()
  • _dirtyfalse,直接返回缓存的 _value(20),不执行 getter → 缓存生效。
4. 依赖变化(num.value 修改)
num.value = 20
  • 修改 num.value 触发 trigger,通知其所有依赖(包括 doubleNum 的 effect);

  • 执行 doubleNum.effect 的调度函数:

    • _dirty 置为 true(标记需要重新计算);
    • 执行 triggerRefValue(this),通知依赖 doubleNum 的地方(比如组件)更新。
5. 再次访问计算属性
console.log(doubleNum.value) // 40
  • _dirtytrue,重新执行 effect.run()

    • 执行 getter 得到新值 40,更新缓存 _value
    • _dirty 置为 false
  • 返回新的缓存值 40。

6. 可写型计算属性的赋值逻辑
doubleNum.value = 30 // 仅可写型生效
  • 触发 ComputedRefImplset value() 方法;
  • 执行用户定义的 setter 函数,修改依赖的响应式数据(比如拆分全名到 firstName/lastName);
  • 依赖数据变化后,重复步骤 4-5,计算属性自动更新。

四、核心设计亮点

  1. lazy effectcomputed 的 effect 是 “懒执行” 的(lazy: true),只有首次访问或依赖变化后访问时,才会执行 getter,避免无用的计算。

  2. _dirty 标记(脏检查) :核心缓存机制的实现:

    • _dirty = true:依赖变化,需要重新计算;
    • _dirty = false:数据 “干净”,直接用缓存。这是计算属性对比普通 effect 的关键区别。
  3. 双层依赖追踪

    • 第一层:计算属性的 effect 依赖基础响应式数据(如 num);
    • 第二层:组件 / 其他 effect 依赖计算属性(如 doubleNum);当基础数据变化时,先标记计算属性为 “脏”,再通知上层依赖更新,保证更新的准确性和性能。
  4. 调度函数(scheduler) :effect 的调度函数不会立即执行 getter,只做两件事:标记 _dirty = true + 触发上层更新,避免不必要的重复计算。

五、关键边界场景的源码处理

  1. 只读计算属性赋值报错:初始化时如果是只读型,setter 被赋值为一个空函数,执行时打印警告,阻止赋值。
  2. effect 失活(组件卸载)ComputedRefImpleffectactive 标记,组件卸载时会将 effect.active = false,停止依赖追踪,避免内存泄漏。
  3. 循环依赖处理:Vue3 对 computed 的循环依赖做了容错处理,不会导致死循环,而是返回当前的缓存值(可能是旧值),并在控制台给出警告。

总结

  1. Vue3 computed 的核心是 ComputedRefImpl 类 + 特殊的 ReactiveEffect,通过 _dirty 标记实现缓存,通过双层依赖追踪实现响应式更新;
  2. 执行流程:初始化(创建 lazy effect)→ 首次访问(执行 getter + 缓存 + 收集依赖)→ 依赖变化(标记 _dirty + 触发上层更新)→ 再次访问(重新计算 / 返回缓存);
  3. 设计亮点:懒执行 effect、脏检查缓存、双层依赖追踪,既保证了响应式准确性,又最大化提升了性能。