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 操作)
相关推荐
然我几秒前
react-router-dom 完全指南:从零实现动态路由与嵌套布局
前端·react.js·面试
一_个前端9 分钟前
Vite项目中SVG同步转换成Image对象
前端
202610 分钟前
12. npm version方法总结
前端·javascript·vue.js
用户876128290737411 分钟前
mapboxgl中对popup弹窗添加事件
前端·vue.js
帅夫帅夫12 分钟前
JavaScript继承探秘:从原型链到ES6 Class
前端·javascript
a别念m12 分钟前
HTML5 离线存储
前端·html·html5
goldenocean43 分钟前
React之旅-06 Ref
前端·react.js·前端框架
子林super1 小时前
【非标】es屏蔽中心扩容协调节点
前端
前端拿破轮1 小时前
刷了这么久LeetCode了,挑战一道hard。。。
前端·javascript·面试
代码小学僧1 小时前
「双端 + 响应式」企业官网开发经验分享
前端·css·响应式设计