computed 和 watch 有什么区别?
简化版
computed 用于根据响应式数据派生新值,有缓存,依赖不变不会重新计算;watch 用于监听数据变化执行副作用,比如请求接口、写缓存、操作非 Vue 系统。能用 computed 表达派生数据时,不要用 watch。
详细版
computed:
const fullName = computed(() => first.value + last.value);
watch:
watch(keyword, async (value) => {
results.value = await search(value);
});
区别:
- computed 关注结果值。
- watch 关注变化过程和副作用。
- computed 有缓存。
- watch 可拿到新旧值,可配置
immediate、deep、flush。
如果一个值可以由已有状态计算出来,优先 computed;如果变化后要做异步请求或外部操作,用 watch。
完整版教学
一、computed 是派生状态
派生状态指不需要单独存储,可以从已有状态算出来的值。比如总价、过滤后的列表、全名。
computed 的好处是缓存。只有依赖变了,才会重新计算。模板中多次使用 computed,不会重复跑 getter。
二、watch 是副作用入口
watch 不强调返回值,而强调“当某个数据变化时做一件事”。
watch(
() => route.params.id,
id => fetchDetail(id),
{ immediate: true }
);
这类请求、日志、同步 URL、调用第三方库,都属于副作用。
三、deep watch 的成本
deep: true 会深度监听对象内部变化,数据结构大时成本较高。很多时候更推荐监听具体字段:
watch(() => form.keyword, handler);
这样依赖更明确,也更可控。
四、watchEffect 的区别
watchEffect 会自动收集回调中用到的响应式依赖,立即执行一次。它适合依赖简单但不想手写来源的场景。
缺点是依赖不如 watch 明确,复杂业务中可读性可能下降。
五、面试追问与工程落地
computed 常见追问是“computed 能不能有 setter”。可以。可写 computed 适合把一个派生值反向拆回多个状态,比如表单里 fullName 拆成 firstName/lastName。但如果 setter 逻辑太复杂,说明状态模型可能需要重新设计。
watch 常见追问是 flush。默认 watch 回调在组件更新前后有特定调度时机;如果需要 DOM 更新后再执行,可以用 flush: 'post';如果要同步执行,可以用 flush: 'sync',但要谨慎,避免破坏批量更新优势。
工程里 watch 请求接口要处理竞态和清理。Vue 的 watch 回调可以使用 cleanup,在下一次回调执行前取消旧请求或忽略旧结果。否则快速输入搜索时,旧请求后返回可能覆盖新结果。
六、从依赖、调度和副作用推演选择
computed 内部是惰性求值的响应式 effect:没人读取时不必计算,读取时若依赖未变就返回缓存,依赖失效后标记为脏并在下次读取时重算。watch 则明确观察一个或多个来源,在值变化后按调度策略执行回调;它的产物不是一个派生值,而是请求、日志、DOM 操作等外部效果。
const price = ref(20)
const count = ref(3)
let runs = 0
const total = computed(() => {
runs++
return price.value * count.value
})
console.log(total.value, total.value, runs) // 60 60 1
| 能力 | computed | watch | watchEffect |
|---|---|---|---|
| 依赖来源 | getter 自动收集 | 显式 source | 同步执行期间自动收集 |
| 是否有返回值缓存 | 有 | 无 | 无 |
| 默认是否立即执行 | 首次读取时 | 否 | 是 |
| 新旧值 | 不以回调形式提供 | 可提供 | 不直接提供 |
| 典型用途 | 派生状态 | 精确副作用 | 依赖与副作用写在一起 |
watch 默认回调通常发生在父组件更新后、所属组件 DOM 更新前;读取更新后的所属组件 DOM 应使用 flush: 'post'。flush: 'sync' 会同步触发,失去批处理保护,若数组同步 push 1000 次就可能带来 1000 次回调,因此只适合非常轻量且变化频率低的来源。
异步 watch 要处理竞态。搜索词从 a 变为 ab 时,旧请求可能在新请求之后返回;应在清理回调中中止旧请求或标记旧结果失效。watchEffect 在异步回调中只会追踪第一个 await 之前同步读取的依赖,不能把 await 后的读取想当然地算进依赖。
判断口诀:需要“一个值”用 computed,需要“发生一件外部事情”用 watch;如果 watch 只是把 A 赋给可计算出的 B,通常是在重复存储派生状态。
七、常见误区与追问
- 误区:computed 每次读取都会重新执行 getter。 依赖未变化时会复用缓存,只有失效并再次读取才重算。
- 误区:watch 适合维护所有派生数据。 这会制造两份状态和同步时序,能够纯计算得到的结果应优先 computed。
- 误区:deep watch 能免费观察大型对象所有变化。 深度遍历会访问嵌套属性,数据越大成本越高,应尽量监听具体来源。
- 追问:watch 和 watchEffect 最大区别是什么? watch 显式指定来源并可比较新旧值;watchEffect 自动收集同步执行中读取的依赖并立即运行。
- 追问:什么时候用 flush post? 回调必须读取所属组件更新后的 DOM 时使用,普通数据副作用通常无需改变默认时机。
- 追问:flush sync 有什么风险? 它对每次同步变化立即触发,不参与常规批处理, 高频写入可能放大执行次数。
- 追问:怎样避免搜索 watch 的请求竞态? 在下一次回调前清理旧副作用,使用 AbortController 或序号只接收最新结果。
八、加强记忆
computed 问“这个值怎么算”,watch 问“变化后要做什么”。派生数据用 computed,异步请求和外部副作用用 watch。
判断时先看结果能否由现有状态纯计算,再看是否需要清理异步副作用和选择回调时机。