跳到主要内容

watch 的原理

watch 是 Vue 里最容易“会用但说不清”的 API 之一。很多人知道它能“监听数据变化”,但再追问:

  • 它和 computed 的区别是什么?
  • 它是怎么知道依赖变了的?
  • 为什么能拿到 newValueoldValue
  • deepimmediateflush 到底做了什么?

就容易开始含糊。

面试速答(30 秒版 TL;DR)

  • watch 的本质是:基于响应式 effect 包一层“懒执行 + 调度 + 回调”的观察器。
  • 它会先把你传入的 source 包装成 getter,执行 getter 时完成依赖收集。
  • source 变化后,不会直接重新渲染,而是由调度器决定何时执行回调。
  • 它之所以能给你 newValueoldValue,是因为内部会保留上一次求值结果。
  • deep 的本质是递归触达深层属性,从而完成深度依赖收集。

1. watch 到底在监听什么?

它监听的不是“变量名”,而是 getter 执行过程中读取到的响应式依赖

例如:

watch(
() => state.count,
(newVal, oldVal) => {
console.log(newVal, oldVal)
}
)

这里真正被监听的是:

  • 执行 () => state.count 时访问到的 state.count

所以 watch 的第一步永远不是“注册回调”,而是“构造 getter”。

2. source 为什么可以有这么多写法?

Vue 允许你传:

  • ref
  • reactive 对象
  • 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

因为它每次执行回调前,都会先保存上一次求值结果。

流程是:

  1. 首次执行 getter,得到 oldValue
  2. 依赖变化
  3. 再执行 getter,得到 newValue
  4. 调用 cb(newValue, oldValue)
  5. newValue 存起来,作为下一轮的 oldValue

所以 oldValue 不是“Vue 从历史快照系统里查出来的”,而是内部自己保留的上一次结果。

5. immediatedeepflush 的本质

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. watchcomputedwatchEffect 的区别

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,再重新求值并把 newValueoldValue 传给回调。

Q2:deep: true 为什么会更慢?

:因为深度监听通常需要递归遍历对象内部属性,目的是让深层字段在 getter 执行时也被读取并收集依赖。对象越大,这个遍历成本越高。

速记要点

  • watch = getter + effect + scheduler + callback
  • oldValue 来自上一次求值缓存
  • deep 本质是递归触达,immediate 是首次补调,flush 是调度时机