2025.09.19
computed的设计和实现:双重角色的响应式节点
💡 核心概念:computed 的双重身份
在响应式系统中,computed 不仅仅是一个简单的计算函数,它是一个拥有 双重身份 的特殊响应式节点:
- 订阅者 (Sub):它订阅其
getter函数内部访问到的所有响应式变量(如ref)。 - 发布者 (Dep):它向所有访问其
.value的effect(或下游computed) 发布更新信号。
| 角色 | 订阅 (Sub) | 发布 (Dep) |
|---|---|---|
computed | 收集其内部 getter 依赖的所有响应式数据 (count)。 | 收集所有访问其 .value 的 effect (effect 函数)。 |
<script type="module">
// import { ref, effect, computed } from '../../../node_modules/vue/dist/vue.esm-browser.prod.js'
import {ref, effect, computed} from '../dist/reactivity.esm.js'
const count = ref(0)
const c = computed(() => { /* 1. 订阅 count */
return count.value + 1
})
effect(() => { /* 2. 订阅 c */
console.log('state.a ==> ', c.value)
})
// ...
</script>

更新流程:
count变更 -> 通知computedcomputed重新计算 -> 通知effecteffect重新执行
🧱 统一接口:ReactiveNode 的诞生
为了统一处理 ref、effect 和 computed 这三类响应式实体,我们将 Dep 和 Sub 签名统一为 ReactiveNode 接口。
/**
* 响应式节点统一接口 (ref, effect, computed 均实现此接口)
*/
export interface ReactiveNode {
// 作为 依赖项 (Dep) 时使用:关联其订阅者 (effect/computed)
subs?: Link | undefined
subsTail?: Link | undefined
// 作为 订阅者 (Sub) 时使用:关联其依赖项 (ref/computed)
deps?: Link
depsTail?: Link
// 标记是否正在追踪依赖,避免重复收集
tracking?: boolean
}
/**
* 链表节点,用于连接 Dep 和 Sub 的桥梁
*/
export interface Link {
dep: ReactiveNode
sub: ReactiveNode // 保存 effect 或 computed
prevSub: Link | undefined
nextSub: Link | undefined
nextDep: Link | undefined
}
⚙️ computed 函数与 ComputedRefImpl 实现
computed函数: 参数归一化computed函数负责处理传入的getter函数或包含get/set的对象,创建ComputedRefImpl实例。import { isFunction } from '@vue/shared' // ... (其他必要的导入) export type ComputedGetter<T> = (oldValue?: T) => T export type ComputedSetter<T> = (newValue: T) => void export interface WritableComputedOptions<T, S = T> { get: ComputedGetter<T> set: ComputedSetter<S> } export function computed<T>(getter: ComputedGetter<T>) export function computed<T, S = T>(options: WritableComputedOptions<T, S>) export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> | undefined if (isFunction(getterOrOptions)) { getter = getterOrOptions } else { getter = getterOrOptions.get setter = getterOrOptions.set } // 创建核心实现类实例 const cRef = new ComputedRefImpl(getter, setter) return cRef as any }ComputedRefImpl: 核心双重角色实现
ComputedRefImpl 类实现了 ReactiveNode 接口,同时处理 Sub 的依赖收集和 Dep 的值读取。
import { activeSub, endTack, link, setActiveSub, startTrack } from './system'
import { Link, ReactiveNode } from './types'
export class ComputedRefImpl<T = any> implements ReactiveNode {
// 用于 isRef 检查
[ReactiveFlags.IS_REF] = true
_value: T // 缓存计算结果
// 作为 Dep:关联订阅者 (effect)
subs: Link | undefined
subsTail: Link | undefined
// 作为 Sub:关联依赖项 (ref/computed)
deps: Link | undefined
depsTail: Link | undefined
tracking = false
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined
) {
}
/**
* 角色一:作为 Dep (发布者)
* 当 effect 读取 .value 时,进行依赖收集。
*/
get value() {
this.update() // 确保值是最新计算的
// 如果存在活动的 effect,则建立当前 computed 到该 effect 的依赖关系
if (activeSub) {
link(this, activeSub)
}
return this._value
}
/**
* 处理可写计算属性的 set 操作
*/
set value(newValue: T) {
if (this.setter) {
this.setter(newValue)
}
else {
console.warn('Write operation failed: computed value is readonly')
}
}
/**
* 角色二:作为 Sub (订阅者)
* 当其依赖项 (如 ref) 发生变化时被调用,重新计算值并收集新的依赖。
*/
update() {
const prevSub = activeSub
// 将当前 computed 实例设为 activeSub,以收集其 getter 内部的依赖
setActiveSub(this)
startTrack(this)
try {
this._value = this.fn() // 重新执行 getter 函数,收集依赖
}
finally {
endTack(this) // 依赖收集结束,清理 deps 链表
setActiveSub(prevSub) // 恢复之前的 effect 上下文
}
}
}

🚨 修复传播机制:区分 effect 与 computed
但是这时候会发现这样一个错误:

在 setTimeout 触发 count.value = 1 后,ref 会调用 propagate。由于 propagate 期望所有订阅者都有一个统一的执行接口(如
run() 或 notify()),而 ComputedRefImpl 只有 update(),因此导致了错误。
修改 propagate 函数,通过检查订阅者实例上是否存在 update 方法来区分 computed 和 effect。
export function propagate(subs: Link): void {
let linkNode = subs
const queuedEffect: ReactiveEffect[] = [] // 用于存储需要异步执行的 effect
while (linkNode) {
const sub = linkNode.sub as ReactiveNode
// 忽略正在追踪依赖的订阅者
if (!sub.tracking) {
if ('update' in sub) {
// 🚀 命中 ComputedRefImpl:立即处理更新
processComputedUpdate(sub as ComputedRefImpl)
}
else {
// 🎯 命中 ReactiveEffect:将其放入队列等待调度
queuedEffect.push(sub as ReactiveEffect)
}
}
linkNode = linkNode.nextSub
}
// 批量处理 queuedEffect (通过调度器 notify)
queuedEffect.forEach(effect => effect.notify())
}
当 ref 通知 computed 更新时,computed 应该立即更新自身的值,并随后通知其下游的所有订阅者。
// system.ts
function processComputedUpdate(sub: ComputedRefImpl): void {
sub.update() // 1. 立即计算 computed 的新值
// 2. 将 computed 自身作为 Dep,通知所有下游订阅者 (如 effect) 更新
if (sub.subs) {
propagate(sub.subs)
}
}
通过这一关键的接口区分和递归传播,我们成功地将 computed 集成到响应式系统中:computed
像水泵一样,在收到上游依赖通知后,立即执行自身计算,并将更新信号传递给下游。