vue3 watch解析

12 阅读6分钟

要理解 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 的核心实现

doWatchwatch 的灵魂函数,负责:

  1. 标准化监听目标(统一处理单个 / 多个、ref/reactive/ 函数等)
  2. 创建 ReactiveEffect 并收集依赖
  3. 处理调度逻辑(时机、防抖、immediate)
  4. 处理 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
}

核心逻辑拆解(新手友好版)

  1. 创建 EffectReactiveEffect(getter, scheduler) 是核心,getter 负责获取监听值并收集依赖,scheduler 负责控制回调执行时机;
  2. 依赖收集:首次执行 effect.run() 时,会调用 getter,访问监听目标的响应式数据,触发 track 收集依赖(把当前 Effect 关联到响应式数据上);
  3. 触发执行:当监听的响应式数据变化时,会调用 trigger,找到关联的 Effect,执行其 scheduler,最终调用 run 函数;
  4. 回调执行run 函数中对比新旧值,只有值变化时才执行用户传入的 cb(回调),并更新旧值。

四、关键细节补充

1. 为什么 reactive 监听拿不到 oldVal?

源码中,oldValue 是通过 getter 获取的,而 reactive 对象是引用类型oldValuenewValue 指向同一个对象,所以无法拿到真正的旧值:

// 监听 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:同步执行 → 不加入队列,数据变化立即执行(性能开销大,慎用)。

总结

  1. watch 本质是带调度器的 ReactiveEffect,依赖 Vue3 的响应式系统(track/trigger)实现监听;
  2. 核心流程:标准化监听目标 → 创建 Effect 收集依赖 → 数据变化触发 scheduler → 对比新旧值执行回调;
  3. 关键特性:deeptraverse 深度遍历收集依赖,immediate 靠首次执行 runflush 靠队列控制执行时机,reactive 监听无 oldVal 是因为引用类型指向同一对象。