在模板里写一行 {{ sum }}
,背后却藏着「惰性求值 + 精准依赖收集 + 脏检查缓存」的三重魔法。本文带你钻进源码,拆解 computed 如何决定「何时算、何时不、为何不能异步」。
一、使用方法
ts
const state = reactive({ a: 1, b: 2 })
const sum = computed(() => state.a + state.b)
console.log(sum.value) // 3
state.a = 10
console.log(sum.value) // 12
看似普通,但注意两点:
- 只读:sum 不是函数,而是一个「带缓存的 getter」。
- 懒执行:直到第一次读取
value
,计算函数才真正跑一遍。
二、缓存机制:dirty 标志位
源码核心只有两行状态机:
ts
let value: any
let dirty = true
- dirty 为 true → 需要重新计算
- dirty 为 false → 直接返回旧值
首次读取 sum.value
时,dirty 从 true 变为 false,并把结果存入 value
。
当依赖的响应式数据变化,调度器把 dirty 重新置为 true,但不会立即计算,而是等待下一次读取。
这就是「缓存」的本质:用 1 bit 的布尔值换一次昂贵的计算。
三、依赖收集:effect 包裹 getter
computed 的计算函数被 effect
包装成副作用:
ts
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, TriggerOpTypes.SET, 'value')
}
})
lazy: true
阻止首次执行,实现惰性求值。scheduler
在依赖变化时只打标记,不立即重算,确保缓存语义。
当模板读取 sum.value
,track
把当前渲染副作用注册到 computed 的依赖图;当 state.a
变化,trigger
通知渲染器重新执行,渲染器再去读 sum.value
,此时才真正触发计算。
四、为什么拒绝异步?
设想一个异步 computed:
ts
const asyncSum = computed(async () => {
const res = await fetch('/api/sum?a=' + state.a)
return res.json()
})
问题立刻暴露:
-
缓存无法兑现
第一次读取返回一个
Promise
,第二次读取依赖并未变化,但缓存里存的是 Promise,无法直接返回结果。 -
渲染时数据缺位
模板在渲染阶段需要同步值,异步导致视图出现空档或闪烁。
-
依赖追踪混乱
异步完成时间不确定,期间若依赖再次变化,无法确定哪一次结果是最新。
Vue 官方给出的替代方案是 watch
+ ref
:
ts
const asyncSum = ref(0)
watch(state, async () => {
asyncSum.value = await fetch('/api/sum?a=' + state.a).then(r => r.json())
})
watch 不缓存、不阻塞渲染,天然适合异步副作用。
五、可写 computed:缓存 + setter 的双通道
ts
const fullName = computed({
get() { return firstName.value + ' ' + lastName.value },
set(v) { [firstName.value, lastName.value] = v.split(' ') }
})
- getter 走同样的缓存逻辑。
- setter 只是普通函数,无缓存要求,因此可以包含异步(但仍不推荐,因为 setter 触发后 getter 需同步返回新值)。
总结
computed 用「dirty 位 + 惰性 effect」实现同步缓存,用「拒绝异步」换取数据一致性。理解了这一点,你就掌握了 Vue 性能调优的第一把钥匙。