前端面试怎么总问 watch 和 computed 的区别?
watch和computed都依赖 Vue 的响应式系统,但它们不是两个“监听数据”的同义 API。面试时先讲职责,再讲缓存、调度、源码和版本差异,答案会比背源码更准确。
一、先了解 Vue 3 响应式
Vue 3 的响应式系统主要基于 Proxy、Reflect、依赖追踪和 scheduler:
读取 reactive/ref
↓ track
建立 effect 与状态的依赖
↓
写入状态
↓ trigger
调度组件更新、computed 或 watcher
Proxy 只是拦截入口,不等于完整的响应式系统。真正的更新还涉及依赖集合、effect 运行栈、清理旧依赖、批量调度和组件 renderer。
Vue 2.7 则主要使用 Object.defineProperty、Dep 和 Watcher。因此,Vue 3 源码中不能简单地说“watch 创建了一个 Watcher 实例”;Vue 3 的内部抽象主要是响应式 effect 和 doWatch 等 watcher API 实现。
二、Watch:监听明确 source,处理副作用
1. 基本用法
<script setup lang="ts">
import { ref, watch } from 'vue'
const count = ref(0)
const doubledCount = ref(0)
watch(count, (newValue, oldValue) => {
console.log({ newValue, oldValue })
// 这里可以请求接口、记录日志或同步第三方对象
doubledCount.value = newValue * 2
})
</script>
<template>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubledCount }}</p>
</template>
这个例子可以帮助理解 watch,但如果 doubledCount 只是 count 的派生值,生产代码应直接使用 computed:
const doubledCount = computed(() => count.value * 2)
watch() 返回停止监听的函数:
const stop = watch(count, () => {})
stop()
2. source 形式
const count = ref(0)
const state = reactive({ user: { name: 'Ada' } })
watch(count, callback)
watch(() => state.user.name, callback)
watch([count, () => state.user.name], callback)
watch(state, callback)
直接把 reactive 对象作为 source 时,Vue 会进行深度观察;使用 getter 返回对象时,默认只在 getter 返回值变化时触发,是否深度追踪需要通过 deep 明确配置。
watch(
() => state.user,
user => {},
{ deep: true },
)
3. 调度、立即执行和清理
watch(
() => route.params.id,
id => loadArticle(id),
{
immediate: true,
flush: 'post',
},
)
immediate:创建 watcher 后先执行一次;deep:递归追踪嵌套对象,可能有明显成本;flush: 'pre':默认调度语义附近;flush: 'post':组件 DOM 更新后执行;flush: 'sync':同步执行,可能失去批量更新优势。
Vue 3.5 的异步清理:
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()
})
注册清理必须发生在第一次 await 之前;更早版本可以使用 watcher 回调的第三个 onCleanup 参数。
三、Computed:缓存的派生状态
import { computed, ref } from 'vue'
const count = ref(0)
const doubledCount = computed(() => count.value * 2)
读取 doubledCount.value 时,computed getter 会按需运行并收集 count 依赖;依赖不变时再次读取会使用缓存。count 变化只会让 computed 失效,通常在下一次读取时重新计算,而不是在 setter 中直接执行 getter。
可写 computed:
const firstName = ref('Ada')
const lastName = ref('Lovelace')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set(value) {
const [first, last = ''] = value.split(' ')
firstName.value = first
lastName.value = last
},
})
computed getter 应保持纯净,不应在其中发请求、修改其它响应式状态或操作 DOM。副作用应交给 watch/watchEffect 或事件处理函数。
四、源码层面的正确说法
1. Vue 3 watch 的源码思路
Vue 3 runtime-core 的 watcher API 会根据 source 创建 getter,然后创建响应式 effect;当依赖变化时由 scheduler 调度 job,job 再执行回调并比较新旧值。可以用以下伪代码表示:
function conceptualWatch(source, callback, options = {}) {
const getter = createGetterFromSource(source, options)
let oldValue = runEffect(getter)
const job = () => {
const newValue = runEffect(getter)
if (options.deep || !Object.is(newValue, oldValue)) {
callback(newValue, oldValue)
oldValue = newValue
}
}
scheduleEffect(getter, job, options.flush)
if (options.immediate) job()
return () => stopEffect(getter)
}
这不是 Vue 源码,真实实现还处理数组 source、深度遍历、cleanup、异步回调、递归更新和错误处理。Vue 3 源码中不应写成 new Watcher(vm, source, cb),那是 Vue 2 语境的简化表达。
2. Vue 3 computed 的源码思路
Vue 3 的 ComputedRefImpl 维护 getter、缓存值、dirty/失效状态和下游依赖。简化模型如下:
class ConceptualComputed {
constructor(getter) {
this.getter = getter
this.dirty = true
this.value = undefined
}
get value() {
trackComputed(this)
if (this.dirty) {
this.value = runEffect(this.getter)
this.dirty = false
}
return this.value
}
invalidate() {
if (!this.dirty) {
this.dirty = true
triggerComputed(this)
}
}
}
原文把 createComputed、ComputedRefImpl 和 Object.defineProperty 混在一起。Vue 3 computed 返回的是 ref-like 对象,通过 .value 的 getter/setter 和响应式 effect 工作;Proxy 是 reactive 对象的主要拦截机制,但不能说 computed 本身“使用 Object.defineProperty 实现数据监听”。
五、watch 和 computed 的核心区别
| 维度 | computed | watch |
|---|---|---|
| 目的 | 得到派生值 | 执行副作用 |
| 是否返回值 | 返回 computed ref | 返回停止监听函数 |
| 缓存 | 默认缓存派生结果 | 不缓存副作用结果 |
| source | getter 自动收集依赖 | 明确指定 source,也可使用 watchEffect 自动收集 |
| 新旧值 | 不以新旧值回调为核心 | 可以拿到 newValue、oldValue |
| 异步 | getter 应保持同步纯净 | 常用于异步请求和清理 |
| DOM 时机 | 作为渲染数据使用 | 可通过 flush 控制执行时机 |
选择口诀:
需要一个“值”,用 computed;变化之后要“做事”,用 watch;不想显式写 source 且不需要旧值,用 watchEffect。
六、Vue 2.7 和 Vue 3.5 的差异
Vue 2.7 Options API:
export default {
data: () => ({
count: 0,
}),
computed: {
doubled() {
return this.count * 2
},
},
watch: {
count: {
immediate: true,
handler(value, oldValue) {
// 副作用
},
},
},
}
Vue 3 Composition API:
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, (value, oldValue) => {})
watchEffect(() => console.log(count.value))
Vue 2 的 $watch 和 Options API 可以使用有限的简单路径字符串;Vue 3 Composition API 应传入 ref、reactive 对象、getter 或 source 数组,不能把任意字符串当作表达式解析。Vue 2.7 虽然回移了部分 Composition API,但底层响应式实现仍然属于 Vue 2 体系。
七、面试建议
- 先说职责,不要一开始背源码;
- 解释 computed 的缓存和惰性求值;
- 解释 watch 的副作用、immediate、deep、flush 和 cleanup;
- 说明 Vue 2 的 Dep/Watcher 与 Vue 3 的 effect/scheduler 不同;
- 给一个“派生值用 computed、异步请求用 watch”的反例和正例;
- 不要把“性能优化”说成 computed 在任何场景都更快,仍需结合依赖数量和更新频率。
参考资料
- Vue 3 Computed
- Vue 3 Watchers
- Vue 3 Reactivity Core API
- Vue 3.5
apiWatch.ts - Vue 3.5
computed.ts - Vue 2.7 computed 和 watchers
原文作者:Bun。原文关于 Vue 3 响应式、源码分析、使用方式和面试建议予以保留,损坏的代码块、错误的 Watcher/Object.defineProperty 归因及版权推广内容已清理。