《Vue3 从入门到大神37篇》Vue3 源码详解(七):watch 与 watchEffect 源码对比——副作用是如何被追踪的?

📅 2026/7/24 10:58:32 👁️ 阅读次数 📝 编程学习
《Vue3 从入门到大神37篇》Vue3 源码详解(七):watch 与 watchEffect 源码对比——副作用是如何被追踪的?

本篇关键词: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

本质

watchEffect

立即执行的副作用 + 自动依赖收集

watch

显式数据源 + 惰性执行 + 新旧值对比 + 调度控制

👉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 = newValue
  • effect.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 的本质,不是“监听数据”,而是“在正确的时机,以正确的方式,执行正确的副作用”