Skip to content

watch 与 watchEffect:侦听器的实现原理与差异对比

reactiverefcomputed 提供了状态派生状态watchwatchEffect 则提供了 ** “状态变化时执行副作用” **的能力(例如 API 请求、DOM 操作)。

本节将分析这两种侦听器的实现原理和核心差异。

侦听器的“双层架构”

Vue 的侦听器设计分为两层:

  1. 底层 (@vue/reactivity):提供 baseWatch 函数,只关心数据订阅与调度,不关心组件。
  2. 上层 (@vue/runtime-core):封装 watchwatchEffect API,增加了与组件生命周期绑定(自动停止)、flush 调度(pre/post/sync)等功能。

我们重点分析上层的 doWatch,它是 watchwatchEffect 的共同实现入口。

watch:精确的侦听器

watch 的核心是:侦听“指定”的数据源,并在其变化时执行“回调函数”

javascript
// 侦听一个 ref
watch(count, (newValue, oldValue) => {
  // ...
})

// 侦听一个 getter 函数
watch(
  () => state.user.name,
  (newName, oldName) => {
    // ...
  }
)

watch 的实现主要分为三步:创建 getter封装 effect定义 job (回调)

创建 getter:定义“侦听什么”

doWatch 首先会根据传入的 source(数据源),将其统一转换为一个 getter 函数。这个 getter 的职责就是“读取数据”。

typescript
// doWatch 内部的 getter 创建逻辑 (简化)
let getter: () => any

if (isRef(source)) {
  // 1. 如果源是 ref
  //    getter 的任务就是读取 .value
  getter = () => source.value 
} else if (isReactive(source)) {
  // 2. 如果源是 reactive 对象
  //    getter 的任务是深度遍历它(默认 deep: true)
  getter = () => traverse(source) 
} else if (isArray(source)) {
  // 3. 如果源是数组
  //    getter 的任务是遍历数组,分别读取每一项
  getter = () => source.map(s => {
    if (isRef(s)) return s.value
    if (isReactive(s)) return traverse(s)
    if (isFunction(s)) return s()
  })
} else if (isFunction(source)) {
  // 4. 如果源已经是 getter 函数
  //    直接使用
  getter = source
}

deep 选项的实现 (traverse): 如果用户设置了 deep: true(对 reactive 对象默认开启),getter 会被再次包装:

typescript
if (cb && deep) {
  const baseGetter = getter
  // 深度遍历,访问对象/数组的每一个属性
  // 目的是在 effect 运行时,“触碰”到所有嵌套属性
  // 从而收集到所有嵌套属性的依赖
  getter = () => traverse(baseGetter())
}

traverse 函数会递归访问一个对象的所有属性,强制 track(收集依赖)它们。

封装 effect:建立响应式连接

有了 getter,Vue 就会用它创建一个 ReactiveEffect

typescript
const effect = new ReactiveEffect(
  // 1. effect 要执行的任务:运行 getter()
  getter, 
  
  // 2. effect 的调度器 (scheduler):
  //    当 getter 依赖的数据变化时,不要立即重新 run()
  //    而是执行这个 scheduler,即我们定义的“回调任务” (job)
  () => job() 
)

这体现了 watchcomputed核心区别

  • computedscheduler 只是为了设置 _dirty = true(标记为脏)。
  • watchscheduler 是为了执行用户定义的回调函数job)。

定义 job (回调):“变化后做什么”

job(任务)是 scheduler 调用的核心,它封装了 watch 回调(cb)的完整执行逻辑。

typescript
// watch 内部的回调任务 (job) (简化)
let oldValue = //... 初始值
let newValue = //...

const job = () => {
  if (!effect.active) {
    return // 侦听器已停止
  }

  if (cb) { // 必须有回调函数 (watchEffect 没有 cb)
    
    // 1. 【重新求值】
    //    再次运行 getter(),获取“新值”
    //    同时也会重建依赖
    newValue = effect.run() 
    
    // 2. 【对比】
    //    对比新旧值 (deep 选项会影响对比)
    if (
      deep ||
      hasChanged(newValue, oldValue)
    ) {
      // 3. 【清理】
      //    如果注册了 onCleanup,先执行上一次的清理函数
      if (cleanup) {
        cleanup()
      }
      
      // 4. 【执行回调】
      //    调用用户传入的 cb(newValue, oldValue, onCleanup)
      cb(newValue, oldValue, onCleanup)
      
      // 5. 【更新旧值】
      oldValue = newValue
    }
  }
}

选项的实现:immediateflush

  • immediate: true 默认情况下,watch 只在数据变化后才执行回调。 如果设置了 immediate: truewatch 会在初始化时立即执行一次 job,此时 oldValueundefined

  • flush: 'pre' | 'post' | 'sync'flush 选项控制 scheduler 调用 job时机

    • synctrigger 发生时,job() 同步执行。(谨慎使用)
    • pre(默认):scheduler 使用 queueJob(job),将 job 放入组件更新前的队列中。
    • postscheduler 使用 queuePostRenderEffect(job),将 job 放入组件更新后的队列中。(适合需要操作 DOM 的回调)

watchEffect:自动收集依赖的侦听器

watchEffect 的实现是 watch 的一种特例

javascript
// watchEffect 是 doWatch 的一种调用方式
export function watchEffect(
  effect: WatchEffect, // 即 source
  options?: WatchEffectOptions,
): WatchHandle {
  // 它调用 doWatch,但:
  // 1. source 就是那个要“立即执行”的函数
  // 2. cb (回调) 为 null
  // 3. options 中,immediate 默认为 true
  return doWatch(effect, null, options)
}

watchEffect 的内部逻辑如下:

  1. getter:就是你传入的那个函数(effect)。
  2. effect:用这个 getter 封装。
  3. job (回调): 由于 cbnulljob 的逻辑简化为:
    javascript
    const job = () => {
      // 1. 【清理】
      //    执行 onCleanup
      if (cleanup) {
        cleanup()
      }
      // 2. 【重新执行】
      //    再次运行 getter(),即用户传入的函数
      //    在运行时,它会自动收集“新”的依赖
      newValue = effect.run() 
    }
    它没有 cbgetter 的执行本身就是“副作用”

onCleanup 的实现watchEffect (或 watchcb) 在执行时,会设置一个全局的 activeWatcheronCleanup 函数会把清理回调注册到这个 activeWatcher 上。当 job 下次执行时,会先检查并调用已注册的 cleanup 函数。

watch vs watchEffect:核心差异对比

特性watchwatchEffect
数据源明确指定 (source)。自动收集 (函数体内用到的所有依赖)。
回调cb 函数。数据源和副作用分离没有 cb。副作用就是 source 函数本身。
立即执行默认惰性lazy: true)。
immediate: true 开启。
总是立即执行immediate: true)。
旧值访问支持cb(newValue, oldValue))。不支持
deepreactive 对象默认 deep: true
getter 函数默认 deep: false
总是深度侦听traverse)。

使用场景对比:

  • 使用 watch 当你...

    1. 需要访问旧值watch(count, (newVal, oldVal) => ...)
    2. 需要惰性执行:只想在数据变化后才执行,不想立即执行。
    3. 想精确控制:只想侦听 state.user.name,而不想侦听 state.user.age
  • 使用 watchEffect 当你...

    1. 想自动收集依赖:你不在乎依赖了多少数据,只要函数体内的任何依赖变了,就重新执行。
    2. 代码更简洁:比如 watchEffect(() => localStorage.setItem('key', state.data))
    3. 只需要“副作用”,不需要“新/旧值”对比。

总结

watchwatchEffect 都是基于 ReactiveEffect 实现的响应式侦听器。

  • watch
    • 分离了“数据源 (getter)”和“副作用 (cb)”
    • 提供 oldValueimmediate: false(惰性)和 deep精确控制
  • watchEffect
    • “数据源”和“副作用”合二为一
    • 立即执行自动收集函数体内的所有依赖。

理解它们都是“创建 getter -> 封装 effect -> 定义 scheduler (即 job)”的产物,能帮助我们根据实际场景选择最合适的 API。


微信公众号二维码

Last updated: