Vue3 的 watch 与 watchEffect 的源码级实现原理与区别深度解析
字数 1412 2025-12-08 03:29:18

Vue3 的 watch 与 watchEffect 的源码级实现原理与区别深度解析

1. 核心概念与基本区别
watchwatchEffect 都是 Vue3 响应式系统中的副作用函数,但它们的设计目标和用法有本质区别:

  • watchEffect:立即执行传入的函数,并自动追踪函数内所有响应式依赖。当任意依赖变化时,重新执行该函数。
  • watch:需要明确指定监听的源(单个或多个响应式值),并在源变化时执行回调函数,可以访问变化前后的值。

2. watchEffect 的源码实现原理

2.1 核心函数入口

function watchEffect(effect, options) {
  return doWatch(effect, null, options)
}
  • effect:要执行的副作用函数
  • options:可选的配置对象(如 flushonTrackonTrigger

2.2 doWatch 函数的统一处理

function doWatch(source, cb, options) {
  // 1. 参数标准化
  const { immediate, deep, flush, onTrack, onTrigger } = options || {}
  
  // 2. 创建响应式依赖的 getter 函数
  let getter
  if (isFunction(source)) {
    // watchEffect 的情况:source 本身就是函数
    getter = () => {
      // 清理上一次的依赖
      if (cleanup) {
        cleanup()
      }
      return source(onCleanup)
    }
  } else {
    // watch 的情况:需要处理其他类型(ref、reactive、函数、数组)
    // ...(watch 专用逻辑)
  }
  
  // 3. 深度监听处理(watchEffect 通常不涉及)
  if (deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  // 4. 旧值存储(watchEffect 不需要)
  let oldValue
  if (cb) {
    // watch 需要存储旧值
    // ...(watch 专用逻辑)
  }
  
  // 5. 调度器函数定义
  const job = () => {
    if (!effect.active) return
    
    if (cb) {
      // watch 的执行逻辑
      // ...(watch 专用逻辑)
    } else {
      // watchEffect 的执行逻辑:直接重新运行 effect
      effect.run()
    }
  }
  
  // 6. 创建响应式 effect
  const effect = new ReactiveEffect(getter, scheduler)
  
  // 7. 初始执行
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  } else {
    // watchEffect:立即执行一次
    effect.run()
  }
  
  // 8. 返回停止函数
  return () => {
    effect.stop()
  }
}

2.3 依赖收集的关键机制
effect.run() 执行时:

  1. 会先调用 effect.fn()(即我们传入的副作用函数)
  2. 在函数执行期间,访问任何响应式属性都会触发 track 操作
  3. 这些被访问的属性都会收集当前 effect 作为依赖
  4. 当这些属性变化时,触发 trigger,重新执行 effect

2.4 清理函数(onCleanup)的实现

function onCleanup(cleanupFn) {
  // 将清理函数挂载到 effect 上
  effect.onStop = () => {
    cleanupFn()
    effect.onStop = null
  }
}
  • 在每次重新执行 effect 前,会先执行上一次的清理函数
  • 用于清理副作用(如取消事件监听、清除定时器等)

3. watch 的源码实现原理

3.1 核心函数入口

function watch(source, cb, options) {
  return doWatch(source, cb, options)
}

3.2 source 参数的多类型处理
doWatch 函数中,针对 watch 的不同用法:

// 处理不同的 source 类型
if (isRef(source)) {
  // 监听单个 ref
  getter = () => source.value
} else if (isReactive(source)) {
  // 监听 reactive 对象
  getter = () => source
  // 默认启用深度监听
  deep = true
} else if (isArray(source)) {
  // 监听数组中的多个源
  getter = () => source.map(s => {
    if (isRef(s)) return s.value
    else if (isReactive(s)) return traverse(s)
    else if (isFunction(s)) return s()
    else return s
  })
} else if (isFunction(source)) {
  // 监听 getter 函数
  getter = source
} else {
  // 无效参数
  getter = NOOP
}

3.3 新旧值对比与回调触发

// job 函数中的 watch 专用逻辑
const job = () => {
  if (!effect.active) return
  
  if (cb) {
    // 执行 getter 获取新值
    const newValue = effect.run()
    
    // 深度监听或值变化时触发回调
    if (deep || hasChanged(newValue, oldValue)) {
      // 执行清理函数
      if (cleanup) {
        cleanup()
      }
      // 触发回调,传入新值、旧值、清理函数
      cb(newValue, oldValue, onCleanup)
      // 更新旧值
      oldValue = newValue
    }
  }
}

3.4 immediate 选项的实现

// 初始执行逻辑
if (cb) {
  if (immediate) {
    // 立即执行回调
    job()
  } else {
    // 只收集依赖,不触发回调
    oldValue = effect.run()
  }
}

4. 调度器(scheduler)与执行时机控制

4.1 flush 选项的实现

let scheduler
if (flush === 'sync') {
  // 同步执行
  scheduler = job
} else if (flush === 'post') {
  // 组件更新后执行(微任务)
  scheduler = () => queuePostFlushCb(job)
} else {
  // 默认:pre(组件更新前执行)
  scheduler = () => {
    if (!instance || instance.isMounted) {
      queuePreFlushCb(job)
    } else {
      // 组件挂载前立即执行
      job()
    }
  }
}

5. 深度监听(deep)的实现原理

5.1 traverse 递归遍历函数

function traverse(value, seen = new Set()) {
  // 1. 基本类型直接返回
  if (!isObject(value) || seen.has(value)) {
    return value
  }
  
  // 2. 避免循环引用
  seen.add(value)
  
  // 3. 递归遍历对象属性
  if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (value instanceof Map) {
    value.forEach((v, k) => {
      traverse(v, seen)
    })
  } else if (value instanceof Set) {
    value.forEach(v => {
      traverse(v, seen)
    })
  } else {
    // 普通对象
    for (const key in value) {
      traverse(value[key], seen)
    }
  }
  
  return value
}

6. 核心区别总结

6.1 依赖收集方式

  • watchEffect:自动收集所有在函数执行期间访问的响应式依赖
  • watch:需要显式指定要监听的源,可以是 ref、reactive、getter 函数或它们的数组

6.2 执行时机

  • watchEffect:立即执行,无法访问旧值
  • watch:默认延迟执行(除非设置 immediate: true),可以访问新旧值

6.3 灵活性

  • watchEffect:更简洁,适合不关心具体哪个依赖变化的场景
  • watch:更精确,可以监听特定值的变化,适合需要旧值的场景

6.4 性能考虑

  • watchEffect:可能收集到不必要的依赖,导致过度触发
  • watch:依赖明确,触发更精确

7. 实际应用场景

7.1 使用 watchEffect

// 自动追踪所有依赖
const count = ref(0)
const text = ref('')

watchEffect(() => {
  console.log(`count: ${count.value}, text: ${text.value}`)
})
// 当 count 或 text 任一变化时都会执行

7.2 使用 watch

// 精确监听特定值
const state = reactive({ count: 0, name: 'Vue' })

watch(
  () => state.count, // 只监听 count
  (newVal, oldVal) => {
    console.log(`count changed from ${oldVal} to ${newVal}`)
  }
)

// 监听多个源
watch(
  [() => state.count, () => state.name],
  ([newCount, newName], [oldCount, oldName]) => {
    // 可以分别处理新旧值
  }
)

8. 内存管理与清理
两个 API 都会返回一个停止函数,用于手动停止监听:

const stop = watchEffect(() => {})
// 需要时停止
stop()

9. 性能优化建议

  1. 对于简单逻辑,优先使用 watchEffect
  2. 需要精确控制依赖时,使用 watch
  3. 使用 onCleanup 及时清理副作用
  4. 避免在 watch 中深度监听大型对象(除非必要)
  5. 合理使用 flush 选项控制执行时机

这个设计体现了 Vue3 响应式系统的灵活性:watchEffect 提供了便捷的自动依赖追踪,而 watch 提供了更精细的控制能力,两者相辅相成,覆盖了不同的使用场景。

Vue3 的 watch 与 watchEffect 的源码级实现原理与区别深度解析 1. 核心概念与基本区别 watch 和 watchEffect 都是 Vue3 响应式系统中的副作用函数,但它们的设计目标和用法有本质区别: watchEffect :立即执行传入的函数,并自动追踪函数内所有响应式依赖。当任意依赖变化时,重新执行该函数。 watch :需要明确指定监听的源(单个或多个响应式值),并在源变化时执行回调函数,可以访问变化前后的值。 2. watchEffect 的源码实现原理 2.1 核心函数入口 effect :要执行的副作用函数 options :可选的配置对象(如 flush 、 onTrack 、 onTrigger ) 2.2 doWatch 函数的统一处理 2.3 依赖收集的关键机制 当 effect.run() 执行时: 会先调用 effect.fn() (即我们传入的副作用函数) 在函数执行期间,访问任何响应式属性都会触发 track 操作 这些被访问的属性都会收集当前 effect 作为依赖 当这些属性变化时,触发 trigger ,重新执行 effect 2.4 清理函数(onCleanup)的实现 在每次重新执行 effect 前,会先执行上一次的清理函数 用于清理副作用(如取消事件监听、清除定时器等) 3. watch 的源码实现原理 3.1 核心函数入口 3.2 source 参数的多类型处理 在 doWatch 函数中,针对 watch 的不同用法: 3.3 新旧值对比与回调触发 3.4 immediate 选项的实现 4. 调度器(scheduler)与执行时机控制 4.1 flush 选项的实现 5. 深度监听(deep)的实现原理 5.1 traverse 递归遍历函数 6. 核心区别总结 6.1 依赖收集方式 watchEffect :自动收集所有在函数执行期间访问的响应式依赖 watch :需要显式指定要监听的源,可以是 ref、reactive、getter 函数或它们的数组 6.2 执行时机 watchEffect :立即执行,无法访问旧值 watch :默认延迟执行(除非设置 immediate: true ),可以访问新旧值 6.3 灵活性 watchEffect :更简洁,适合不关心具体哪个依赖变化的场景 watch :更精确,可以监听特定值的变化,适合需要旧值的场景 6.4 性能考虑 watchEffect :可能收集到不必要的依赖,导致过度触发 watch :依赖明确,触发更精确 7. 实际应用场景 7.1 使用 watchEffect 7.2 使用 watch 8. 内存管理与清理 两个 API 都会返回一个停止函数,用于手动停止监听: 9. 性能优化建议 对于简单逻辑,优先使用 watchEffect 需要精确控制依赖时,使用 watch 使用 onCleanup 及时清理副作用 避免在 watch 中深度监听大型对象(除非必要) 合理使用 flush 选项控制执行时机 这个设计体现了 Vue3 响应式系统的灵活性: watchEffect 提供了便捷的自动依赖追踪,而 watch 提供了更精细的控制能力,两者相辅相成,覆盖了不同的使用场景。