本篇关键词: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 的本质,不是"监听数据",而是**"在正确的时机,以正确的方式,执行正确的副作用"**。