2025.09.20
computed:缺陷修复与惰性求值实现
前文我们虽然初步实现了 computed 的双重角色,但在实际应用中,它暴露了两个核心缺陷:冗余执行 和 不必要的更新传播
,导致其性能远低于官方实现。
前文我们虽然初步实现了 computed 的双重角色,但在实际应用中,它暴露了两个核心缺陷:冗余执行 和 不必要的更新传播
,导致其性能远低于官方实现。
🚨 缺陷一:计算函数的冗余执行
<script type="module">
// ...
const count = ref(0)
const c = computed(() => {
console.count('computed') // 实际执行了 3 次!
return count.value + 1
})
effect(() => {
console.log('state.a ==> ', c.value)
})
setTimeout(() => {
count.value = 1
})
</script>
官方 Vue 实现中,computed 仅会执行两次:
- 初始化:
effect首次执行时访问c.value -> computed执行 - 依赖变更:
count.value = 1触发computed重新执行。
我们的实现中多出的第 3 次执行,正是因为 缺乏缓存
解决方式: 引入 dirty 脏检查
- 状态定义: 在
ComputedRefImpl中新增dirty = true。 - 惰性检查: 在
get value()访问器中,仅当dirty为true时才调用update()重新计算。 - 更新后缓存:
update()执行完毕后,立即设置this.dirty = false。
// ComputedRefImpl.ts 核心逻辑
export class ComputedRefImpl implements ReactiveNode {
dirty = true // 标记是否需要重新计算
get value() {
if (this.dirty) { // 检查脏标记
this.update()
}
// ... 依赖收集逻辑 ...
return this._value
}
update() {
// ... 收集依赖的 setup 逻辑 ...
try {
// 重新执行 getter 函数
this._value = this.fn()
this.dirty = false // 缓存新值,标记为 clean
// ...
}
finally {
// ...
}
}
}
⚠️ 缺陷二:无订阅者的 computed 不应执行
<script type="module">
// ...
const c = computed(() => {
console.count('computed') // 不应该在 setTimeout 中执行
return count.value + 1
})
// effect(...) <-- 这一部分被注释掉
setTimeout(() => {
count.value = 1 // 此时 c 应该只标记为 dirty,不执行 getter
})
</script>
当 count.value = 1 触发 propagate 时,computed (c) 被找到。由于我们之前的 propagate 逻辑如下:
- 找到
c,发现它是computed。 - 调用
processComputedUpdate(c)。 processComputedUpdate调用c.update()-> 导致console.count('computed')立即执行。
解决方式: 在 propagate 阶段遇到 computed 节点时,新增一个对 该节点自身订阅者 (sub.subs) 的检查。
- 统一标记: 无论如何,先将
computed节点标记为dirty = true。 - 条件判断: 只有在
sub.subs链表不为空(即有effect或其他computed依赖它)时,才调用processComputedUpdate立即更新并向下游传播。
// system.ts
function processComputedUpdate(sub: ComputedRefImpl) {
if (sub.subs) {
sub.update()
propagate(sub.subs)
}
}
export function propagate(subs: Link): void {
let linkNode = subs
const queuedEffect = []
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking && !sub.dirty) {
if ('update' in sub) {
sub.dirty = true
processComputedUpdate(sub as ComputedRefImpl)
}
else {
queuedEffect.push(sub)
}
}
linkNode = linkNode.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
🎯 陷阱三:计算结果不变时的冗余 effect 执行
即使上游依赖(count)发生变化,如果 computed 的返回值没有改变(例如 count.value * 0),下游的 effect
也不应该被触发执行。然而,当前的实现中 effect 仍会被执行两次。
<script type="module">
// ...
const count = ref(0)
const c = computed(() => {
console.count('computed');
return count.value * 0
})
effect(() => {
console.count('effect');
console.log('state.a ==> ', count.value);
count.value
})
setTimeout(() => {
count.value = 1
}) // effect 仍执行 2 次
</script>
解决方案: 通过在 ComputedRefImpl.update() 中返回 hasChanged 的结果,并在 processComputedUpdate 中利用该结果来决定是否调用
propagate。
// computed.ts (ComputedRefImpl 内部逻辑)
class ComputedRefImpl implements ReactiveNode {
update() {
try {
const oldValue = this._value
this._value = this.fn()
// 核心:返回新旧值是否发生变化
return hasChanged(this._value, oldValue)
}
finally {
// ... 清理逻辑
}
}
}
// system.ts (processComputedUpdate 逻辑)
function processComputedUpdate(sub: ComputedRefImpl) {
/**
* 1. 调用 update() 重新计算,并获取返回值 (changed: boolean)
* 2. 仅在 subs 链表存在 且 值发生变化时,才向下游传播
*/
if (sub.subs && sub.update()) {
propagate(sub.subs)
}
}
💥 陷阱四:重复依赖收集导致的冗余 effect 执行
在同一个 effect 函数体内,如果多次访问同一个 ref(如 count.value 访问 5 次),由于重复收集了依赖,当 count 变化时,
effect 可能会被冗余通知多次执行。
<script type="module">
// ...
effect(() => {
count.value // 重复访问 5 次
})
setTimeout(() => {
count.value = 1
}) // effect 触发 3 次
</script>
解决方式:
- 源码里是在
link函数中, 建立Dep-Sub关联之前,遍历effect (Sub)的依赖链表 (deps),确认是否已存在对该ref (Dep)的引用。export function link(dep: ReactiveNode, sub: ReactiveNode): void { const currentDep = sub.depsTail const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep if (nextDep && nextDep.dep === dep) { sub.depsTail = nextDep return } /** * 如果之前创建过依赖关系, 直接返回, 避免依赖收集 */ let existingLink = sub.deps while (existingLink) { if (existingLink.dep === dep) { return } existingLink = existingLink.nextDep } } - 优化脏检查的逻辑
换种思路 -> 不管是否是重复创建依赖, 而是确保
effect在一次更新周期内只入队执行一次
// effect.ts
export class ReactiveEffect {
// ... (其他属性)
dirty = false // 统一调度锁:true 表示已被处理/已入队
}
// system.ts
export function propagate(subs: Link): void {
// ...
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking && !sub.dirty) {
sub.dirty = true
if ('update' in sub) {
// computed 的处理
processComputedUpdate(sub as ComputedRefImpl)
}
else {
queuedEffect.push(sub)
}
}
linkNode = linkNode.nextSub
}
// ...
}
export function endTrack(sub) {
sub.tracking = false
const depsTail = sub.depsTail
sub.dirty = false
}