本系列前三篇我们已经拆解了
reactive、collection handlers、effect体系,本篇继续深入 Vue3 响应式系统的第四个核心:computed 派生值 。本文依旧沿用统一结构:概念 → 架构 → 核心流程 → 源码逐段解析 → 与 effect 的关系 → 使用示例 → 扩展 → 潜在问题。
一、Computed 是什么?(概念篇)
在 Vue3 的响应式系统中,computed 属于 派生类型(Derived State) :
- 由其他响应式值计算得来
- 自动追踪依赖
- 自动缓存(不重复执行 getter)
- 支持可写/只读两种模式
- 与
effect解耦,不再使用 effect 实现
在新版实现中,computed 更像一个 "双重身份单元" :
- 作为 被动目标:被外部 effect 读取时,需要收集依赖(自身被依赖)
- 作为 主动订阅者:自己要订阅其他响应式源,以便在源更新时"变脏"
这种结构使得 computed 更轻、更快、更可控。
二、Computed 的总体架构
在 Vue3 中,computed 的主要结构如下:
objectivec
ComputedRefImpl
├─ _value // 缓存的计算结果
├─ fn // getter
├─ setter // 可写 computed 才有
├─ dep // 哪些 effect 依赖 computed
├─ deps / depsTail // computed 依赖哪些 ref/reactive(反向链)
├─ flags // 当前 computed 是否 dirty
├─ globalVersion // 版本号控制是否要重新计算
├─ isSSR // SSR 模式不同逻辑
其中最关键的是 flags(dirty) + globalVersion(版本校验)
这两个共同控制 computed 的缓存刷新逻辑。
三、核心流程:Computed 的运行机制
Vue3 中 computed 工作流程可以分为四个步骤:
1)初始化
- 创建
ComputedRefImpl - 记录 getter
- 若有 setter 则为可写模式
- 标记为 dirty(需要首次计算)
2)首次访问 .value
- 触发
dep.track()收集依赖 - 触发
refreshComputed(this) - 执行 getter 得到新值
- 缓存
_value - 清除 dirty / 更新版本号
3)依赖更新 → 触发 notify()
如果 computed 所依赖的数据改变:
- computed.flags |= DIRTY
- 下一次访问
.value时会重新执行 getter
4)再次访问 .value
若 dirty = false 且版本号未变:
→ 不重新计算,直接返回缓存
若 dirty = true 或版本号变化:
→ 重新计算
四、源码逐段解析(与你前三篇风格完全一致)
以下代码全部来自你给的源码,我将按模块拆解并解释。
① 依赖导入
javascript
import { isFunction } from '@vue/shared'
import {
DebuggerEvent,
DebuggerOptions,
EffectFlags,
Subscriber,
activeSub,
batch,
refreshComputed,
} from './effect'
import { warn } from './warning'
import { Dep, Link, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'
解析
batch()→ 批量通知 effect,避免频繁触发refreshComputed()→ computed 的核心刷新逻辑Dep→ 用于记录计算属性被哪些 effect 依赖globalVersion→ 全局版本号(优化)
computed 不再依赖 effect,而是依赖 Dep(更轻量)。
② ComputedRefImpl 类定义
typescript
export class ComputedRefImpl<T = any> implements Subscriber {
computed 自身是一个 Subscriber(订阅者)
它能订阅其他 reactive/ref。
③ 内部字段
ini
_value: any = undefined
dep: Dep = new Dep(this)
__v_isRef = true
__v_isReadonly: boolean
deps?: Link = undefined
depsTail?: Link = undefined
flags: EffectFlags = EffectFlags.DIRTY
globalVersion: number = globalVersion - 1
isSSR: boolean
字段解析
| 字段 | 作用 |
|---|---|
| _value | 缓存的计算结果 |
| dep | 谁依赖了这个 computed |
| deps | computed 依赖的响应式源(反向) |
| flags | 是否 dirty,需要重新计算 |
| globalVersion | 避免重复计算的版本号控制 |
④ 构造函数
kotlin
constructor(fn, setter, isSSR) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
没有 setter → readonly computed。
这部分设计和前三篇响应式系统一致。
⑤ notify:依赖发生变化时触发 dirty
kotlin
notify() {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
activeSub !== this
) {
batch(this, true)
return true
}
}
原理说明
当依赖更新时:
- 标记本 computed 为 dirty
- 通过 batch 通知依赖它的 effect
- 避免 self-recursion(避免自己递归触发自己)
这与上一篇 Collection 里的触发机制一致。
⑥ get value:读取并刷新缓存
kotlin
get value() {
const link = this.dep.track()
refreshComputed(this)
if (link) {
link.version = this.dep.version
}
return this._value
}
核心关键点(非常重要)
- dep.track()
表示 "外部 effect 正在依赖我" - refreshComputed(this)
若 dirty 或版本号变化 → 执行 getter - 返回缓存值
这是 computed 靠缓存优化性能的关键。
⑦ set value:支持可写 computed
kotlin
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else {
warn('Write operation failed: computed value is readonly')
}
}
这个很好理解。
⑧ computed() 工厂函数
ini
export function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter
let setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}
return cRef
}
支持两种形式
- computed(getter)
- computed({ get, set })
同 Vue3 文档。
五、Computed 与 Effect 的关系
Vue3 中:
| 项目 | Vue2 | Vue3 |
|---|---|---|
| computed 内部实现 | 基于 watcher | 不再使用 effect |
| 依赖收集 | watcher.deps | dep + version |
| 缓存策略 | lazy watcher | version + dirty |
Computed 更轻、更纯,不会参与 effect 的副作用队列。
六、实战示例
scss
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
count.value++
console.log(plusOne.value) // 3(重新计算)
使用体验不变,但实现更加高效。
七、扩展:Computed 为什么要分版本号?
原因:提升性能、减少不必要计算
例如:
多个 ref 改变但 getter 内只依赖其中一个时,版本号能避免无效计算。
Vue3 响应式系统中 version 机制广泛存在:
- ref
- reactive
- computed
都依赖 version。
八、潜在问题与注意事项
- getter 必须是纯函数(不要做副作用)
- computed 不适合包含异步(会变成 Promise)
- 多层嵌套 computed 可能产生链式 dirty --- Vue 已做优化
- setter 写入不要和 getter 逻辑矛盾
总结
本篇从 computed 的整体设计、数据结构、dirty 机制、版本号策略、依赖链结构,到源码逐段拆解,完整讲解了 Vue3 计算属性的底层实现,使其风格与前三篇完全保持一致。
本文部分内容借助 AI 辅助生成,并由作者整理审核。