No.110
computed 和 watch 有什么区别?
一句话回答
computed 用于从已有状态派生出新值:带缓存,依赖不变时多次读取不会重新计算;是惰性的,被读取时才计算;getter 应该是没有副作用的纯函数。watch 用于在数据变化时执行副作用,比如发请求、操作 DOM、写入本地存储,能拿到新值和旧值,也可以执行异步操作。能用 computed 表达的,就不要用 watch 去手动同步另一个状态。
详细解析
对比
| computed | watch | |
|---|---|---|
| 用途 | 根据已有状态派生新值 | 数据变化时执行副作用 |
| 返回值 | 一个 ref,默认只读 | 用来停止监听的函数 |
| 缓存 | 有,依赖不变就返回上次的结果 | 无 |
| 执行时机 | 惰性,被读取时才计算 | 数据源变化后执行回调,默认创建时不执行 |
| 异步 | 不应该,getter 要同步返回结果 | 可以 |
| 新值和旧值 | 无(Vue 3.4 起 getter 能拿到上一次的计算结果) | 回调参数中有 |
computed
- 缓存基于响应式依赖:依赖变化时只做标记,下次读取时才重新计算,原理详见 Vue 3 的响应式原理
- 在模板中调用方法也能得到同样的结果,但方法每次渲染都会执行一遍,
computed只在依赖变化后才重新计算 - getter 中不要修改其他状态、发异步请求或操作 DOM
- 可以通过
get/set定义可写的计算属性,在set中修改源数据。比如"全选"复选框:get判断是否每一项都已勾选,set把每一项都设为同样的状态
watch
数据源可以是 ref、reactive 对象、getter 函数,或者由它们组成的数组。
- 直接监听一个 reactive 对象时,会隐式开启深度监听,任何嵌套属性变化都会触发回调
- 监听 reactive 对象的某个属性时要写成 getter,例如
watch(() => state.count, cb);写成watch(state.count, cb)传进去的只是一个数字,监听不到变化
| 选项 | 作用 |
|---|---|
immediate |
创建时立即执行一次回调 |
deep |
深度监听;Vue 3.5 起可以传数字,限制遍历的最大深度 |
flush |
回调的执行时机:默认 'pre',在所属组件的 DOM 更新前执行;'post' 在 DOM 更新后执行;'sync' 在数据变化时同步执行 |
once |
Vue 3.4 起,回调只执行一次 |
watchEffect:不需要指定数据源,创建时立即执行一次,并自动收集回调中用到的响应式数据作为依赖。它只追踪同步执行部分访问的数据,第一个 await 之后访问的数据不会被收集。
停止、暂停和清理
watch和watchEffect都返回一个函数,调用它就能停止监听。在setup中同步创建的侦听器会在组件卸载时自动停止,在异步回调中创建的要手动停止- Vue 3.5 起,这个返回值上还有
pause()和resume()方法,可以暂停和恢复监听。暂停期间如果数据变了,恢复后会补执行一次回调 - 清理副作用:用回调的第三个参数
onCleanup注册清理函数,它会在下一次回调执行前、以及侦听器停止时调用,适合取消上一次还没完成的请求。Vue 3.5 起也可以从vue中导入onWatcherCleanup,但它只能在回调的同步部分调用,不能放在await之后
代码示例
搜索框:关键词变化时发请求,并取消上一次还没完成的请求,避免旧请求后返回、覆盖新结果。
Vue
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const results = ref([])
watch(keyword, async (value, oldValue, onCleanup) => {
if (!value.trim()) {
results.value = []
return
}
const controller = new AbortController()
// 关键词再次变化时,会先执行这里注册的清理函数,取消这次请求
onCleanup(() => controller.abort())
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(value)}`, {
signal: controller.signal,
})
results.value = await res.json()
} catch (err) {
// 被取消的请求会抛出 AbortError,忽略即可
if (err.name !== 'AbortError') console.error(err)
}
})
</script>
<template>
<input v-model="keyword" placeholder="搜索文章" />
<ul>
<li v-for="item in results" :key="item.id">{{ item.title }}</li>
</ul>
</template>
面试官可能追问
computed 里能发请求吗?
不应该。computed 的 getter 只负责根据依赖算出一个值,它是惰性、带缓存的,什么时候执行、执行几次取决于它什么时候被读取,放进副作用会让请求时机不可控。而且 getter 必须同步返回结果,拿不到异步请求的返回值。正确的做法是用 watch 监听参数,请求完成后把结果写进一个 ref,展示用的数据再用 computed 从这个 ref 派生。
监听一个 reactive 对象默认是深度监听吗?
是的,直接把 reactive 对象作为数据源时会隐式深度监听。但下面两种情况默认不是:
- 数据源是返回对象的 getter,如
() => state.user,只有state.user被整体替换时才触发,修改state.user.name不会触发,需要加deep: true - 用
ref包着一个对象,watch(userRef, cb)默认只在.value被替换时触发
深度监听要遍历所有嵌套属性,对象很大时有性能开销。
怎么处理 watch 中的请求竞态?
竞态是指先发出的请求后返回,用旧结果覆盖了新结果。处理方法:
- 用
onCleanup(或 Vue 3.5 的onWatcherCleanup)在下一次回调执行前取消上一次请求,如上面代码中的AbortController - 请求无法取消时,在清理函数中把一个"已过期"标记设为
true,请求返回后检查这个标记,过期就丢弃结果 - 配合防抖,从源头上减少请求次数,详见 防抖和节流
易错点
watch(state.count, cb)监听不到变化,要写成watch(() => state.count, cb)- 对象被原地修改时,深度监听回调中的新值和旧值指向同一个对象,无法直接对比
watchEffect只收集同步部分的依赖,await之后用到的数据变化时不会触发它
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。