vue3源码解析:watch的实现

在上文中,我们分析了 Vue 的计算属性实现。本文我们将分析 watch 的实现原理。watch 是 Vue 中另一个重要的响应式特性,它允许我们监听响应式数据的变化并执行相应的回调函数。

一、示例引入

让我们从一个简单的 watch 示例开始:

vue 复制代码
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 基础用法:监听单个数据源
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 监听多个数据源
watch([count, message], ([newCount, newMsg], [oldCount, oldMsg]) => {
  console.log(`count: ${oldCount} -> ${newCount}`)
  console.log(`message: ${oldMsg} -> ${newMsg}`)
})

// 监听响应式对象
const user = reactive({
  name: 'John',
  age: 20
})

// deep 选项:深度监听对象的所有属性变化
watch(user, (newValue, oldValue) => {
  console.log('user changed:', newValue)
}, { deep: true })

// immediate 选项:立即执行一次回调
watch(count, (newValue) => {
  console.log('immediate watch:', newValue)
}, { immediate: true })
</script>

这个例子展示了 watch 的几个重要特性:

  • 多种数据源支持:可以监听 ref、reactive 对象、getter 函数等
  • 多数据源监听:可以同时监听多个数据源的变化
  • 深度监听:通过 deep 选项可以监听对象的深层属性变化
  • 立即执行:通过 immediate 选项可以在创建时立即执行一次
  • 清理函数:回调函数的第三个参数允许注册清理函数

二、核心实现分析

2.1 watch 函数的入口

watch 函数的实现相对复杂,它需要处理多种不同类型的数据源和选项。让我们看看它的源码实现:

js 复制代码
export function watch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb?: WatchCallback | null,
  options: WatchOptions = EMPTY_OBJ
): WatchHandle {
  const { 
    immediate, 
    deep, 
    once, 
    scheduler,
    // 内部选项
    augmentJob,
    call 
  } = options

  // 处理无效的数据源
  const warnInvalidSource = (s: unknown) => {
    ;(options.onWarn || warn)(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`
    )
  }

  // ... 后续实现
}

2.2 数据源处理

watch 函数首先需要处理不同类型的数据源,将它们统一转换为 getter 函数:

js 复制代码
let getter: () => any
let forceTrigger = false
let isMultiSource = false

// 1. 处理 ref 类型
if (isRef(source)) {
  getter = () => source.value
  forceTrigger = isShallow(source)
}
// 2. 处理 reactive 对象
else if (isReactive(source)) {
  getter = () => reactiveGetter(source)
  forceTrigger = true
}
// 3. 处理数组(多数据源)
else if (isArray(source)) {
  isMultiSource = true
  forceTrigger = source.some(s => isReactive(s) || isShallow(s))
  getter = () =>
    source.map(s => {
      if (isRef(s)) {
        return s.value
      } else if (isReactive(s)) {
        return reactiveGetter(s)
      } else if (isFunction(s)) {
        return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
      } else {
        __DEV__ && warnInvalidSource(s)
      }
    })
}
// 4. 处理函数
else if (isFunction(source)) {
  if (cb) {
    // getter with cb
    getter = call
      ? () => call(source, WatchErrorCodes.WATCH_GETTER)
      : (source as () => any)
  } else {
    // watchEffect
    getter = () => {
      if (cleanup) {
        pauseTracking()
        try {
          cleanup()
        } finally {
          resetTracking()
        }
      }
      const currentEffect = activeWatcher
      activeWatcher = effect
      try {
        return call
          ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
          : source(boundCleanup)
      } finally {
        activeWatcher = currentEffect
      }
    }
  }
}
// 5. 处理无效数据源
else {
  getter = NOOP
  __DEV__ && warnInvalidSource(source)
}

2.3 深度监听的实现

当设置了 deep 选项时,watch 会递归遍历被监听的对象:

js 复制代码
if (cb && deep) {
  const baseGetter = getter
  const depth = deep === true ? Infinity : deep
  getter = () => traverse(baseGetter(), depth)
}

traverse 函数实现了深度遍历:

js 复制代码
export function traverse(
  value: unknown,
  depth: number = Infinity,
  seen?: Set<unknown>
): unknown {
  if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }

  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  depth--

  if (isRef(value)) {
    traverse(value.value, depth, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], depth, seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, depth, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse(value[key], depth, seen)
    }
  }
  return value
}

2.4 副作用创建与调度

watch 内部会创建一个 ReactiveEffect 来追踪依赖变化:

js 复制代码
const job = (immediateFirstRun?: boolean) => {
  if (!(effect.flags & EffectFlags.ACTIVE) || (!effect.dirty && !immediateFirstRun)) {
    return
  }
  if (cb) {
    // watch(source, cb)
    const newValue = effect.run()
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
        : hasChanged(newValue, oldValue))
    ) {
      // 执行清理函数
      if (cleanup) {
        cleanup()
      }
      const currentWatcher = activeWatcher
      activeWatcher = effect
      try {
        const args = [
          newValue,
          // 首次执行时传递 undefined 作为旧值
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
              ? []
              : oldValue,
          boundCleanup
        ]
        call
          ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
          : cb!(...args)
        oldValue = newValue
      } finally {
        activeWatcher = currentWatcher
      }
    }
  } else {
    // watchEffect
    effect.run()
  }
}

// 创建 effect 实例
effect = new ReactiveEffect(getter)

// 配置调度器
effect.scheduler = scheduler
  ? () => scheduler(job, false)
  : (job as EffectScheduler)

2.5 清理函数的处理

watch 支持在回调函数中注册清理函数,这些清理函数会在下次回调执行前被调用:

js 复制代码
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()

export function onWatcherCleanup(
  cleanupFn: () => void,
  failSilently = false,
  owner: ReactiveEffect | undefined = activeWatcher
): void {
  if (owner) {
    let cleanups = cleanupMap.get(owner)
    if (!cleanups) cleanupMap.set(owner, (cleanups = []))
    cleanups.push(cleanupFn)
  } else if (__DEV__ && !failSilently) {
    warn(
      `onWatcherCleanup() was called when there was no active watcher` +
        `to associate with.`
    )
  }
}

三、总结

通过以上分析,我们详细了解了 Vue watch 的实现原理,主要包括以下几个方面:

  1. 数据源处理

    • 统一转换为 getter 函数
    • 支持 ref、reactive、函数等多种类型
    • 支持多数据源监听
  2. 响应式更新

    • 基于 ReactiveEffect 实现依赖收集
    • 通过调度器控制回调执行时机
    • 提供清理函数机制避免副作用残留
  3. 与计算属性的区别

    • 计算属性:用于数据派生,有缓存,惰性执行
    • watch:用于副作用处理,无缓存,主动执行

watch 和计算属性虽然都基于 Vue 的响应式系统,但各自有其适用场景:

  • 计算属性适合用于数据转换
  • watch 适合用于执行副作用(如异步请求、DOM 操作)
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax