要理解 computed 的底层实现,核心要抓住两个关键点:响应式依赖追踪 和 缓存机制。Vue3 的 computed 源码位于 packages/reactivity/src/computed.ts 文件中,以下从核心逻辑、关键类 / 函数、执行流程三个维度拆解。
一、核心前置知识
在看 computed 源码前,需先了解 Vue3 响应式的两个基础模块:
- Effect(副作用) :
computed本质是一个特殊的effect,effect是响应式系统的核心,用于追踪依赖、触发更新; - Ref/Track/Trigger:
track用于收集依赖,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
-
触发
ComputedRefImpl的get value()方法; -
执行
trackRefValue(this):收集当前执行上下文对doubleNum的依赖(比如组件渲染 effect); -
此时
_dirty为true,执行effect.run():- 执行 getter 函数
() => num.value * 2; - 访问
num.value时,num的track会收集doubleNum的 effect 作为依赖; - 将 getter 返回值(20)赋值给
_value(缓存),并将_dirty置为false;
- 执行 getter 函数
-
返回缓存值
_value(20)。
3. 重复访问计算属性
console.log(doubleNum.value) // 20
- 触发
get value(); _dirty为false,直接返回缓存的_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
-
_dirty为true,重新执行effect.run():- 执行 getter 得到新值 40,更新缓存
_value; _dirty置为false;
- 执行 getter 得到新值 40,更新缓存
-
返回新的缓存值 40。
6. 可写型计算属性的赋值逻辑
doubleNum.value = 30 // 仅可写型生效
- 触发
ComputedRefImpl的set value()方法; - 执行用户定义的
setter函数,修改依赖的响应式数据(比如拆分全名到 firstName/lastName); - 依赖数据变化后,重复步骤 4-5,计算属性自动更新。
四、核心设计亮点
-
lazy effect:
computed的 effect 是 “懒执行” 的(lazy: true),只有首次访问或依赖变化后访问时,才会执行 getter,避免无用的计算。 -
_dirty 标记(脏检查) :核心缓存机制的实现:
_dirty = true:依赖变化,需要重新计算;_dirty = false:数据 “干净”,直接用缓存。这是计算属性对比普通 effect 的关键区别。
-
双层依赖追踪:
- 第一层:计算属性的 effect 依赖基础响应式数据(如
num); - 第二层:组件 / 其他 effect 依赖计算属性(如
doubleNum);当基础数据变化时,先标记计算属性为 “脏”,再通知上层依赖更新,保证更新的准确性和性能。
- 第一层:计算属性的 effect 依赖基础响应式数据(如
-
调度函数(scheduler) :effect 的调度函数不会立即执行 getter,只做两件事:标记
_dirty = true+ 触发上层更新,避免不必要的重复计算。
五、关键边界场景的源码处理
- 只读计算属性赋值报错:初始化时如果是只读型,
setter被赋值为一个空函数,执行时打印警告,阻止赋值。 - effect 失活(组件卸载) :
ComputedRefImpl的effect有active标记,组件卸载时会将effect.active = false,停止依赖追踪,避免内存泄漏。 - 循环依赖处理:Vue3 对 computed 的循环依赖做了容错处理,不会导致死循环,而是返回当前的缓存值(可能是旧值),并在控制台给出警告。
总结
- Vue3
computed的核心是ComputedRefImpl类 + 特殊的ReactiveEffect,通过_dirty标记实现缓存,通过双层依赖追踪实现响应式更新; - 执行流程:初始化(创建 lazy effect)→ 首次访问(执行 getter + 缓存 + 收集依赖)→ 依赖变化(标记 _dirty + 触发上层更新)→ 再次访问(重新计算 / 返回缓存);
- 设计亮点:懒执行 effect、脏检查缓存、双层依赖追踪,既保证了响应式准确性,又最大化提升了性能。