watch 的原理
watch 是 Vue 里最容易“会用但说不清”的 API 之一。很多人知道它能“监听数据变化”,但再追问:
- 它和 computed 的区别是什么?
- 它是怎么知道依赖变了的?
- 为什么能拿到
newValue和oldValue? deep、immediate、flush到底做了什么?
就容易开始含糊。
面试速答(30 秒版 TL;DR)
watch的本质是:基于响应式 effect 包一层“懒执行 + 调度 + 回调”的观察器。- 它会先把你传入的 source 包装成 getter,执行 getter 时完成依赖收集。
- source 变化后,不会直接重新渲染,而是由调度器决定何时执行回调。
- 它之所以能给你
newValue和oldValue,是因为内部会保留上一次求值结果。 deep的本质是递归触达深层属性,从而完成深度依赖收集。
1. watch 到底在监听什么?
它监听的不是“变量名”,而是 getter 执行过程中读取到的响应式依赖。
例如:
watch(
() => state.count,
(newVal, oldVal) => {
console.log(newVal, oldVal)
}
)
这里真正被监听的是:
- 执行
() => state.count时访问到的state.count
所以 watch 的第一步永远不是“注册回调”,而是“构造 getter”。
2. source 为什么可以有这么多写法?
Vue 允许你传:
refreactive对象- getter 函数
- source 数组
这是因为内部会先做一层标准化。
比如:
watch(countRef, cb)
watch(() => state.count, cb)
watch([foo, () => bar.value], cb)
最终都会被整理成统一可执行的 getter 形式。
3. 内部是怎么收集依赖的?
可以把 watch 理解成“创建了一个懒执行 effect”。
简化伪代码:
let oldValue
const getter = normalizeSource(source)
const job = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
const effect = new ReactiveEffect(getter, job)
oldValue = effect.run()
这里核心有三点:
getter负责读取依赖effect.run()负责触发依赖收集job负责真正调用用户回调
4. 为什么 watch 能拿到 oldValue?
因为它每次执行回调前,都会先保存上一次求值结果。
流程是:
- 首次执行 getter,得到
oldValue - 依赖变化
- 再执行 getter,得到
newValue - 调用
cb(newValue, oldValue) - 把
newValue存起来,作为下一轮的oldValue
所以 oldValue 不是“Vue 从历史快照系统里查出来的”,而是内部自己保留的上一次结果。
5. immediate、deep、flush 的本质
5.1 immediate
默认情况下,watch 是“先收集,再等变化”。
开启 immediate: true 后,会立刻执行一次回调。
但注意:
- 这不是“依赖先变了”
- 而是框架主动补了一次首次调用
5.2 deep
如果只是 watch 一个对象引用:
watch(() => state.user, cb)
那只有 state.user 的引用变了才会触发。
而 deep: true 的核心做法,是递归遍历对象内部属性,让 getter 在执行时触达深层字段,从而把这些字段也纳入依赖收集。
5.3 flush
flush 决定回调执行时机:
pre:组件更新前post:组件更新后sync:同步执行
这其实是把 watch 回调放进不同的调度队列。
6. cleanup 是怎么工作的?
watch(source, (value, oldValue, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
})
每次回调重新执行前,Vue 会先执行上一次注册的 cleanup。
这很适合处理:
- 请求竞态
- 定时器清理
- 事件解绑
7. watch 和 computed、watchEffect 的区别
| API | 核心用途 | 是否懒执行 | 是否拿到 oldValue | 依赖收集方式 |
|---|---|---|---|---|
computed | 派生值 | 是 | 否 | 自动 |
watch | 监听特定源并做副作用 | 是 | 是 | 手动指定 source |
watchEffect | 立即运行副作用 | 否 | 否 | 自动 |
一句话区分:
- 要结果值:
computed - 要精确监听某个源:
watch - 要副作用自动收集依赖:
watchEffect
8. 常见坑
8.1 误以为 watch 监听的是“变量名”
不是,它监听的是 getter 读取到的响应式依赖。
8.2 深度 watch 滥用
deep: true 可能递归遍历大量对象,成本不低。能精确监听具体字段,就不要全量 deep。
8.3 sync 滥用
同步执行虽然“看起来更直接”,但容易打破批量调度节奏,实际项目一般更谨慎使用。
9. 面试高频答法
Q1:watch 的原理是什么?
答:本质上是创建一个包装后的响应式 effect。它先把 source 变成 getter,执行 getter 时收集依赖;依赖变化后由 scheduler 触发 job,再重新求值并把 newValue、oldValue 传给回调。
Q2:deep: true 为什么会更慢?
答:因为深度监听通常需要递归遍历对象内部属性,目的是让深层字段在 getter 执行时也被读取并收集依赖。对象越大,这个遍历成本越高。
速记要点
watch = getter + effect + scheduler + callbackoldValue来自上一次求值缓存deep本质是递归触达,immediate是首次补调,flush是调度时机