优雅回答 watch 和 computed 的区别和选择?
原文主要以 Vue 2 Options API 为背景。下面保留“定义—场景—细节—原理”的面试回答结构,并补充 Vue 2.7、Vue 3.5 的 API 差异。
一、面试官想考察什么
这个问题通常可以从四个方面回答:
computed和watch的定义及使用差异;- 不同场景下如何选择;
- 缓存、深度监听、异步清理等细节;
- Vue 2 和 Vue 3 的 API、响应式实现差异。
二、computed 和 watch 的区别
1. computed:从状态得到派生值
计算属性用于根据已有响应式状态计算出另一个值。它应该尽量是纯函数,不负责请求接口、修改其他状态或执行日志等副作用。
<script setup lang="ts">
import { computed, ref } from 'vue'
const firstName = ref('周')
const lastName = ref('杰伦')
const fullName = computed(() => `${firstName.value}${lastName.value}`)
</script>
<template>
<p>{{ fullName }}</p>
</template>
computed 默认具有缓存性:依赖没有变化时,重复读取会复用上一次的结果。它和 methods 的区别可以这样回答:
<script setup lang="ts">
import { computed, ref } from 'vue'
const count = ref(1)
const doubleCount = computed(() => count.value * 2)
function getDoubleCount() {
return count.value * 2
}
</script>
<template>
<!-- doubleCount 在依赖不变时可以复用缓存 -->
<p>{{ doubleCount }}</p>
<!-- methods 每次渲染执行到这里都会调用函数 -->
<p>{{ getDoubleCount() }}</p>
</template>
缓存不是“任何情况下都更快”。如果计算非常简单,缓存管理本身也有成本;是否使用应由语义和实际更新频率决定。
2. watch:监听变化并执行副作用
watch 需要明确指定监听源,当源变化后执行回调,适合:
- 发起或取消异步请求;
- 把状态同步到
localStorage、URL 或第三方库; - 记录日志、埋点;
- 在一个状态变化后修改另一个状态;
- 需要拿到新值和旧值的场景。
<script setup lang="ts">
import { ref, watch } from 'vue'
const keyword = ref('')
watch(keyword, async (newKeyword, oldKeyword) => {
console.log({ newKeyword, oldKeyword })
// 请求、持久化或其他副作用
})
</script>
“watch 没有返回值”是不准确的。Vue 3 的 watch() 返回停止监听的函数,Vue 2 的 $watch() 也会返回 unwatch 函数:
const stop = watch(keyword, () => {
// ...
})
stop()
watch 的回调可以是异步函数,但 watch 本身不是“异步版 computed”,它的核心区别仍然是副作用与派生值。
三、如何选择
可以用一句话概括:
要得到一个值,优先考虑
computed;状态变化后要做事情,使用watch;只想自动使用当前依赖而不关心旧值,可以考虑watchEffect。
适合 computed 的场景
const activeUsers = computed(() => {
return users.value.filter(user => user.active)
})
- 过滤、排序、格式化列表;
- 计算总价、全名、权限结果;
- 根据多个状态得到一个模板使用的值;
- 需要缓存的同步派生数据。
适合 watch 的场景
watch(
() => route.params.id,
id => loadArticle(id),
{ immediate: true },
)
- 路由参数变化后重新请求;
- 搜索关键词变化后 debounce 请求;
- 监听表单并保存草稿;
- 与地图、图表、编辑器等非 Vue 对象同步。
不要为了“有变化就执行代码”而把所有逻辑都写成 watch。如果逻辑只是把 a 和 b 计算为 c,computed 更容易维护,也更少产生状态同步问题。
四、常用细节
1. 可读写 computed
computed 默认只有 getter,也可以显式提供 getter 和 setter:
import { computed, ref } from 'vue'
const firstName = ref('周')
const lastName = ref('杰伦')
const fullName = computed({
get: () => `${firstName.value}${lastName.value}`,
set: value => {
const [nextFirstName, ...rest] = value.trim().split(' ')
firstName.value = nextFirstName
lastName.value = rest.join(' ')
},
})
Options API 写法:
export default {
data: () => ({
firstName: '周',
lastName: '杰伦',
}),
computed: {
fullName: {
get() {
return `${this.firstName}${this.lastName}`
},
set(value) {
// 根据业务规则拆分并更新 data
},
},
},
}
setter 不是让 computed 变成一个独立状态,它仍然应该把写操作转发到真实状态。
2. immediate
默认情况下,watch 回调只在监听源变化后执行;immediate: true 会在创建监听时先执行一次:
watch(
() => props.id,
id => loadArticle(id),
{ immediate: true },
)
3. deep
监听对象的某个嵌套变化时,可以使用深度监听:
watch(
form,
value => saveDraft(value),
{ deep: true },
)
深度监听需要遍历或追踪大量嵌套依赖,数据树很大时可能产生明显开销。不要把“字符串形式监听”当成通用优化方案,更推荐精确监听真正需要的叶子路径:
watch(
() => form.user.name,
name => saveName(name),
)
Vue 3.5 还支持把 deep 设置为数字,限制遍历深度:
watch(source, callback, { deep: 2 })
数字深度适合有明确数据层级的场景,仍然需要结合数据规模测试。
4. watchEffect
watchEffect 会立即执行一次,并自动收集回调同步执行期间访问的响应式依赖:
import { watchEffect } from 'vue'
const stop = watchEffect(() => {
document.title = `${count.value} 条消息`
})
它适合依赖较少且不需要旧值的副作用。需要精确控制监听源、immediate、deep 或新旧值时,使用 watch 更清晰。
watchEffect 的异步回调只会自动跟踪第一个 await 之前访问的依赖:
watchEffect(async () => {
const id = currentId.value // 会被跟踪
await fetch(`/api/articles/${id}`)
// await 之后新读取的响应式值不会作为本次自动依赖
})
5. 执行时机和清理
Vue 3 的 watcher 默认会在组件更新前批量执行,可以通过 flush 调整时机:
watch(source, callback, { flush: 'post' })
watchSyncEffect(() => {
// 同步执行,使用前要谨慎
})
flush: 'post' 适合需要读取组件更新后 DOM 的场景;但复杂 DOM 交互通常仍建议在逻辑中明确使用 await nextTick()。
异步请求需要处理竞态和清理。Vue 3.5 可以使用 onWatcherCleanup,它必须在异步回调第一次 await 之前注册:
import { onWatcherCleanup, watch } from 'vue'
watch(searchText, async value => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort())
const response = await fetch(`/api/search?q=${value}`, {
signal: controller.signal,
})
results.value = await response.json()
})
也可以使用回调的第三个参数 onCleanup,它对需要兼容较早 Vue 3 版本的代码更友好:
watch(source, async (value, oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
// ...
})
五、Vue 2.7 与 Vue 3.5 的 API 差异
Vue 2.7 Options API
export default {
data: () => ({
firstName: '周',
lastName: '杰伦',
keyword: '',
}),
computed: {
fullName() {
return `${this.firstName}${this.lastName}`
},
},
watch: {
keyword: {
immediate: true,
handler(value, oldValue) {
// 执行副作用
},
},
// Vue 2 支持简单的点分隔路径,不支持任意表达式
'user.profile.name'(value) {
// ...
},
},
}
Vue 3 Composition API
import { computed, ref, watch } from 'vue'
const firstName = ref('周')
const lastName = ref('杰伦')
const keyword = ref('')
const fullName = computed(() => `${firstName.value}${lastName.value}`)
watch(keyword, value => {
// 副作用
})
watch(
() => user.profile.name,
value => {
// Composition API 使用 getter 表达路径
},
)
Vue 3 Composition API 的 watch source 不是任意字符串表达式。原文“Vue 3 去掉了点操作符之外的字符串形式”需要改成:Vue 2 的 $watch/Options API 可以使用有限的简单路径;Composition API 应传入 ref、reactive 对象、getter 或 source 数组,不能把任意字符串当作表达式解析。
Vue 2.7 回移了部分 Composition API,但其底层响应式仍属于 Vue 2 的 getter/setter 体系;不能因为都能写 watch() 就认为两代 watcher 内部完全相同。
六、computed 的原理
6.1 Vue 2.7:lazy Watcher 和 Dep
Vue 2 的响应式链路可以简化为:
Observer/defineReactive
↓ 读取时收集
Dep
↓ 通知
Watcher
computed 的 watcher 具有 lazy/dirty 语义:
- 创建 computed watcher 时不立即执行 getter;
- 第一次访问 computed 时执行 getter,收集它依赖的响应式属性;
- 依赖变化后将 watcher 标记为 dirty;
- 下一次访问时重新计算并缓存;
- 外层渲染 watcher 读取 computed 时,computed 还会把依赖通知转发出去。
因此,“computed 在 data 的 setter 中直接重新计算”是不准确的。setter 负责通知依赖,computed 通常在下一次访问时惰性求值。
6.2 Vue 3.5:ComputedRef 和响应式 effect
Vue 3 的 computed() 返回一个 ref-like 对象:读取 .value 时进行依赖收集,依赖变化时让 computed 失效或触发调度,下一次读取时再按需求值。其具体内部类和 Dep/Link 结构属于实现细节,不应把 Vue 2 的 Watcher 源码直接套到 Vue 3。
七、面试版总结
computed用于从响应式状态得到派生值,getter 应保持纯净,并且具有缓存;watch用于监听明确的响应式源,在变化后执行副作用,可以拿到新旧值、执行异步逻辑并配置 immediate/deep/flush 等选项。Vue 2.7 常见实现是 Observer、Dep 和 lazy Watcher;Vue 3.5 使用 Composition API 的computed、watch、watchEffect,对象响应式底层主要基于 Proxy。需要“算出一个值”选 computed,需要“发生变化后做事情”选 watch。
参考资料
原文作者:Dignity_呱。原文主题和面试问答结构予以保留,版本边界、watch 清理和 computed 原理已按 Vue 2.7/Vue 3.5 重新整理。