要理解 watch 的底层逻辑,我们需要从 Vue3 的响应式系统和 watch 核心实现源码入手。以下解析基于 Vue3 源码(packages/runtime-core/src/apiWatch.ts),聚焦核心逻辑,剔除边缘分支。
一、先理清核心依赖
watch 的实现完全依赖 Vue3 的响应式核心:
- Effect 系统:
watch本质是一个带调度器的副作用函数(ReactiveEffect) - 依赖收集:通过
track收集监听目标的依赖,数据变化时通过trigger触发 Effect - 调度器(scheduler) :控制 Effect 执行时机(如
flush: pre/post/sync)、防抖(默认只执行最后一次)
二、watch 核心入口函数
Vue3 暴露的 watch 函数是一个封装后的入口,核心定义在 apiWatch.ts 中,简化后的核心逻辑如下:
// 核心入口:用户调用的 watch 函数
export function watch<T = any>(
source: WatchSource<T> | WatchSource<T>[], // 监听目标(ref/reactive/函数等)
cb: WatchCallback<T>, // 用户传入的回调函数
options?: WatchOptions // 配置项(immediate/deep/flush 等)
): WatchStopHandle { // 返回停止监听的函数
// 标准化配置项(设置默认值:flush: 'pre'、deep: false、immediate: false)
const resolvedOptions = resolveWatchOptions(options)
// 核心:创建 watcher 实例
const instance = getCurrentInstance() // 获取当前组件实例
const effect = doWatch( // 真正实现 watch 逻辑的核心函数
source,
cb,
resolvedOptions,
instance
)
// 返回停止监听的函数(本质是停止 effect)
return () => {
effect.stop()
}
}
核心结论:watch 函数只是一层封装,真正的逻辑在 doWatch 中,最终返回的 “停止函数” 本质是停止内部的 ReactiveEffect。
三、doWatch:watch 的核心实现
doWatch 是 watch 的灵魂函数,负责:
- 标准化监听目标(统一处理单个 / 多个、ref/reactive/ 函数等)
- 创建 ReactiveEffect 并收集依赖
- 处理调度逻辑(时机、防抖、immediate)
- 处理 deep 深度监听
1. 第一步:标准化监听目标
首先把用户传入的各种监听目标(ref、reactive、数组、函数)统一为获取值的函数(getter) ,简化后的核心代码:
function doWatch(
source: WatchSource | WatchSource[] | WatchCallback,
cb: WatchCallback | null,
options: WatchOptions,
instance: ComponentInternalInstance | null
) {
// 1. 标准化监听目标为 getter 函数(核心:统一不同类型的 source)
let getter: () => any
const isMultiSource = isArray(source) // 是否监听多个目标
if (isMultiSource) {
// 监听多个目标:getter 返回所有目标的值组成的数组
getter = () => source.map(s => normalizeWatchSource(s))
} else if (isRef(source)) {
// 监听 ref:getter 返回 ref.value
getter = () => source.value
} else if (isReactive(source)) {
// 监听 reactive:开启深度监听 + getter 返回自身
getter = () => source
options.deep = true // reactive 强制开启 deep(用户传 false 也无效)
} else if (isFunction(source)) {
// 监听函数(如 () => user.age):getter 直接用这个函数
getter = () => source.call(instance && instance.proxy, instance)
} else {
// 无效目标:getter 为空,不监听
getter = NOOP
warn(`无效的 watch 监听目标:${source}`)
}
// 2. 处理 deep 深度监听:重写 getter,递归遍历对象收集所有依赖
if (options.deep) {
const baseGetter = getter
// 重写 getter:调用 traverse 递归遍历值,触发所有深层属性的依赖收集
getter = () => traverse(baseGetter())
}
// ... 后续逻辑见下文
}
// 辅助函数:标准化单个监听源
function normalizeWatchSource(source: WatchSource): any {
if (isRef(source)) {
return source.value
} else if (isReactive(source)) {
return source
} else if (isFunction(source)) {
return source()
} else {
return NOOP
}
}
// 核心:深度遍历对象,触发所有属性的 track(依赖收集)
function traverse(value: unknown, seen = new Set()) {
if (!isObject(value) || seen.has(value)) {
return value
}
seen.add(value)
// 遍历对象/数组的所有属性,递归触发访问(收集依赖)
if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (isPlainObject(value)) {
for (const key in value) {
traverse((value as any)[key], seen)
}
}
return value
}
关键解释:
- 无论用户传入什么类型的监听目标,最终都会被转为一个
getter函数,watch内部只需要执行这个函数就能拿到监听值; deep: true的本质是调用traverse递归遍历对象的所有属性,触发每个属性的track(依赖收集),这样哪怕是深层属性变化,也能触发 watch;- 监听
reactive对象时,Vue 会强制开启 deep(因为 reactive 本身是深层响应式的)。
2. 第二步:创建 ReactiveEffect 并处理调度
这是 doWatch 的核心,创建副作用函数并关联调度器,简化后的代码:
function doWatch(
source: WatchSource | WatchSource[] | WatchCallback,
cb: WatchCallback | null,
options: WatchOptions,
instance: ComponentInternalInstance | null
) {
// ... 省略第一步:标准化 getter
// 2. 定义副作用函数的调度器(控制回调执行时机/防抖)
let scheduler: EffectScheduler
const { flush } = options
if (flush === 'sync') {
// 同步执行:数据变化立即触发回调
scheduler = () => run(cb)
} else if (flush === 'post') {
// 组件更新后执行:加入 post 队列(比如 watch 中访问更新后的 DOM)
scheduler = () => queuePostEffect(run, instance && instance.suspense)
} else {
// 默认 flush: 'pre':组件更新前执行
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreEffect(run, instance)
} else {
// 组件未挂载时直接执行
run()
}
}
}
// 3. 创建 ReactiveEffect(核心:依赖收集 + 触发执行)
// effect 执行时会调用 getter,从而收集依赖
const effect = new ReactiveEffect(getter, scheduler)
// 4. 处理 immediate:立即执行一次回调
if (options.immediate) {
// 立即执行回调(此时 oldValue 为 undefined)
run()
} else {
// 非 immediate:先执行一次 effect(仅收集依赖,不执行回调)
effect.run()
}
// 5. 定义真正执行回调的 run 函数
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
function run() {
if (effect.active) {
// 执行 getter 获取新值(触发依赖收集)
const newValue = effect.run()
// 对比新旧值,变化则执行回调
if (
deep ||
isMultiSource
? newValue.some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)
) {
// 执行用户传入的回调:cb(newVal, oldVal)
cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue)
// 更新旧值
oldValue = newValue
}
}
}
// 6. 返回停止监听的函数(停止 effect,清空依赖)
return effect
}
核心逻辑拆解(新手友好版)
- 创建 Effect:
ReactiveEffect(getter, scheduler)是核心,getter负责获取监听值并收集依赖,scheduler负责控制回调执行时机; - 依赖收集:首次执行
effect.run()时,会调用getter,访问监听目标的响应式数据,触发track收集依赖(把当前 Effect 关联到响应式数据上); - 触发执行:当监听的响应式数据变化时,会调用
trigger,找到关联的 Effect,执行其scheduler,最终调用run函数; - 回调执行:
run函数中对比新旧值,只有值变化时才执行用户传入的cb(回调),并更新旧值。
四、关键细节补充
1. 为什么 reactive 监听拿不到 oldVal?
源码中,oldValue 是通过 getter 获取的,而 reactive 对象是引用类型,oldValue 和 newValue 指向同一个对象,所以无法拿到真正的旧值:
// 监听 reactive 时,getter 返回的是对象本身(引用)
getter = () => source // source 是 reactive 对象
// 所以 oldValue = newValue = 同一个对象引用
解决方案:如果需要旧值,要手动深拷贝,或监听具体属性(() => user.age)。
2. watch 的防抖逻辑(默认行为)
Vue3 的 watch 默认是防抖的:如果短时间内数据多次变化,只会执行最后一次回调。核心原因:scheduler 会把 run 函数加入队列,队列会做去重 + 防抖,确保同一 watch 只执行最后一次。
3. flush 执行时机的底层逻辑
pre(默认):组件更新前执行 → 加入preFlushQueue,在组件patch前执行;post:组件更新后执行 → 加入postFlushQueue,在组件patch后执行(可访问更新后的 DOM);sync:同步执行 → 不加入队列,数据变化立即执行(性能开销大,慎用)。
总结
watch本质是带调度器的 ReactiveEffect,依赖 Vue3 的响应式系统(track/trigger)实现监听;- 核心流程:标准化监听目标 → 创建 Effect 收集依赖 → 数据变化触发 scheduler → 对比新旧值执行回调;
- 关键特性:
deep靠traverse深度遍历收集依赖,immediate靠首次执行run,flush靠队列控制执行时机,reactive监听无 oldVal 是因为引用类型指向同一对象。