在 Vue 的响应式宇宙里,数据变化最终都会触发「副作用」:可能是重新渲染 DOM、发送网络请求,也可能是打印一条日志。Vue 提供了两条官方路线来捕获这些副作用------computed
与 watch
。它们共享同一套依赖追踪引擎,却以截然不同的姿态服务于业务。
一、computed:把「结果」缓存成状态
想象你在做一份实时报表:销售额 = 单价 × 数量。只要单价或数量发生变化,报表必须立即更新,但你不希望每次模板读取都去重算一次。
ts
const price = ref(100)
const count = ref(5)
const total = computed(() => price.value * count.value)
total
不是函数,而是一个「带缓存的 getter」。核心逻辑只有两步:
- 首次读取 → 执行函数并将结果缓存;
- 依赖变动 → 把缓存标记为「脏」,等待下一次读取时重新计算。
因此,total
永远同步、永远幂等、永远无副作用。它像数学公式一样纯粹,最适合放到模板里直接展示。
二、watch:把「动作」注册成回调
现在需求升级:当销售额首次超过 1000 时,弹一个提示,并向后端发一条记录。
这个动作需要「副作用」------弹窗与网络请求------这正是 watch
的舞台。
ts
watch(total, (newVal, oldVal) => {
if (newVal > 1000 && oldVal <= 1000) {
alert('破千啦!')
fetch('/api/record', { method: 'POST', body: newVal })
}
})
watch
不关心结果,只关心「变化」。它把每一次变动封装成事件,允许你执行任意副作用:
- 可以同步,也可以异步;
- 可以读写 DOM,可以调用接口;
- 不缓存任何结果,因为结果往往无法预测。
三、底层实现:同一条 effect,两种调度策略
无论是 computed
还是 watch
,最终都会被包进同一个 effect
函数。差异体现在 调度器(scheduler):
- computed 的调度器
ts
scheduler() {
dirty = true // 仅仅标记,不立即计算
trigger(obj, 'value') // 通知渲染函数
}
它把真正的计算推迟到「下一次读取」,保证缓存生效。
- watch 的调度器
ts
scheduler() {
const newVal = effectFn()
cb(newVal, oldValue) // 立即执行用户回调
oldValue = newVal
}
它把最新值直接喂给用户回调,不缓存、不等待。
同一颗依赖追踪引擎,通过两个调度器分化出「缓存结果」与「立即动作」两条路径。
总结
用一句口诀:
- 需要结果 → computed
- 需要动作 → watch
具体场景举例:
- 表单校验规则 ------ 用 computed,规则是数据推导;
- 校验失败后高亮输入框 ------ 用 watch,高亮是副作用;
- 根据路由参数计算面包屑 ------ 用 computed,面包屑是结果;
- 路由变化后滚动到顶部 ------ 用 watch,滚动是动作。