本篇关键词:watch、watchEffect、job、scheduler、flush、pre / post / sync、cleanup
难度等级:⭐⭐⭐⭐⭐
工程价值:watch 是 Vue3 中最复杂的响应式 API。读懂它,你才能真正理解“副作用调度”“异步更新”“竞态处理”这些高阶工程问题。
一、开篇:为什么 watch 比 computed 复杂?
很多同学觉得:
“watch 不就是监听数据变化吗?”
但真实情况是:
为什么
watchEffect会自动收集依赖?为什么
watch需要明确指定 source?为什么
flush: 'post'能拿到更新后的 DOM?为什么 watch 能拿到新旧值,而 watchEffect 不行?
为什么 watch 能防止“竞态问题”?
👉答案不在 API 层面,而在调度系统(Scheduler)中。
二、一句话区分 watch 和 watchEffect
API | 本质 |
|---|---|
| 立即执行的副作用 + 自动依赖收集 |
| 显式数据源 + 惰性执行 + 新旧值对比 + 调度控制 |
👉watch 是 watchEffect 的“增强版”。
三、watchEffect 源码解析:自动依赖收集的真相
1️⃣ 使用方式
watchEffect(() => { console.log(state.count) })特点:
✅ 立即执行
✅ 自动收集依赖
❌ 拿不到旧值
❌ 无法精确控制触发时机
2️⃣ watchEffect 的核心实现(简化版)
源码位置:packages/runtime-core/src/apiWatch.ts
function watchEffect( effectFn, options = {} ) { return doWatch(effectFn, null, options) }👉所有魔法,都在doWatch里。
3️⃣ watchEffect 的 effect 创建
const effect = new ReactiveEffect(effectFn) effect.scheduler = () => { if (flush === 'sync') { job() } else { queueJob(job) } }关键点:
watchEffect内部也是一个ReactiveEffect通过
scheduler接入 Vue 的异步调度系统默认是异步执行(
flush: 'pre')
4️⃣ 为什么 watchEffect 能自动收集依赖?
effect.run()执行effectFn时:
读取响应式数据
触发
track建立依赖关系
👉和普通 effect 没有任何区别。
四、watch 源码解析:精确控制的艺术
1️⃣ watch 的使用方式
watch( () => state.count, (newVal, oldVal) => { console.log(newVal, oldVal) }, { flush: 'post', immediate: true } )特点:
✅ 明确指定 source
✅ 拿到新旧值
✅ 支持
immediate✅ 支持
deep✅ 支持
flush控制
2️⃣ watch 的核心实现(简化版)
function watch(source, cb, options) { return doWatch(source, cb, options) }👉没错,watch 和 watchEffect 共用同一个doWatch。
区别在于:有没有 callback。
五、doWatch:watch 系统的真正大脑
1️⃣ source 的三种形态
Vue3 支持三种 watch source:
// 1️⃣ getter watch(() => state.count, cb) // 2️⃣ ref watch(countRef, cb) // 3️⃣ reactive watch(state, cb, { deep: true })源码中统一处理:
let getter: () => any if (isRef(source)) { getter = () => source.value } else if (isReactive(source)) { getter = () => source deep = true } else if (isFunction(source)) { getter = source }2️⃣ job:watch 回调的执行单元
const job = () => { if (cb) { const newValue = effect.run() if (deep || hasChanged(newValue, oldValue)) { cb(newValue, oldValue) oldValue = newValue } } else { effect.run() } }👉新旧值对比发生在 job 中,而不是 trigger 中。
3️⃣ 为什么 watch 能拿到旧值?
核心秘密:
let oldValue = initialValue oldValue = newValueeffect.run()得到新值与缓存的旧值对比
符合条件才执行回调
✅这就是 watch 能精准控制更新的原因。
六、flush 机制:pre / post / sync 的本质
这是 watch 最难理解,但工程价值最高的部分。
1️⃣ flush: 'pre'(默认)
flush: 'pre'执行时机:
组件更新前源码逻辑:
queuePreFlushCb(job)✅适合:大多数业务逻辑
2️⃣ flush: 'post'
flush: 'post'执行时机:
组件更新后 → DOM 已更新源码逻辑:
queuePostRenderEffect(job, instance)✅适合:操作 DOM、获取尺寸、Scroll 位置
watch(source, cb, { flush: 'post' }) onMounted(() => { // 等价于 watch + post })3️⃣ flush: 'sync'
flush: 'sync'执行时机:
数据变化 → 立即执行源码逻辑:
job()⚠️慎用:可能导致多次同步更新,性能差。
七、deep watch:性能与精度的权衡
1️⃣ 为什么 reactive 默认 deep?
watch(state, cb)因为:
getter = () => source track(state, GET, ITERATE_KEY)👉一次性追踪整个对象。
2️⃣ deep 的实现原理
function traverse(value, seen = new Set()) { if (!isObject(value) || seen.has(value)) return seen.add(value) for (const key in value) { traverse(value[key], seen) } return value }👉递归访问每一个属性,强制触发 track。
⚠️性能代价极高,慎用 deep。
八、cleanup:解决竞态问题的关键
1️⃣ 竞态问题示例
watch(id, async (newId) => { const res = await fetchData(newId) data.value = res })如果id快速变化:
请求 A 发出
请求 B 发出
B 先返回,A 后返回 →数据错乱
2️⃣ watch 的 cleanup 机制
watch(id, async (newId, oldId, onCleanup) => { let canceled = false onCleanup(() => { canceled = true }) const res = await fetchData(newId) if (!canceled) { data.value = res } })源码本质:
const cleanup = effect.onStop || (() => {})👉每次重新执行前,先执行上一次的 cleanup。
✅这是前端“竞态控制”的标准解法。
九、watchEffect vs watch:源码级对比表
维度 | watchEffect | watch |
|---|---|---|
依赖收集 | 自动 | 显式 |
立即执行 | ✅ | 可选 |
新旧值 | ❌ | ✅ |
深度监听 | ❌ | ✅ |
调度控制 | 基础 | 精细 |
竞态处理 | ❌ | ✅ |
适用场景 | 副作用 | 数据变化响应 |
十、调试实战:观察 watch 的调度过程
在 VSCode 中打这些断点:
packages/runtime-core/src/apiWatch.ts ├── doWatch() ├── job() ├── queueJob() └── cleanup()测试代码:
watchEffect(() => { console.log('effect', state.count) }) watch(() => state.count, (n, o) => { console.log('watch', n, o) }) state.count++观察:
queueJob何时入队job何时执行DOM 更新前后顺序
👉你会彻底理解 Vue3 的异步更新机制。
十一、工程最佳实践
✅ 优先使用 watchEffect
watchEffect(() => { document.title = state.title })✅ DOM 相关用 flush: 'post'
watch(element, () => { el.getBoundingClientRect() }, { flush: 'post' })✅ 异步请求必用 cleanup
watch(id, (newId, _, onCleanup) => { const abort = new AbortController() onCleanup(() => abort.abort()) fetch(url, { signal: abort.signal }) })❌ 避免 deep watch
// ❌ 性能差 watch(state, cb, { deep: true }) // ✅ 精确监听 watch(() => state.user.name, cb)十二、本篇总结
✅ watch / watchEffect 共用doWatch
✅ 调度系统(queueJob)是异步更新的核心
✅ flush 控制执行时机:pre / post / sync
✅ cleanup 是解决竞态问题的关键
✅ deep watch 有显著性能代价
一句话升华:
watch 的本质,不是“监听数据”,而是“在正确的时机,以正确的方式,执行正确的副作用”。