Vue3源码:5个问题带你读懂watch

前言

Vue3Composition API中,watch被广泛使用,无论是在表单监听、数据同步,还是监听路由变化,watch 都是Vue开发中不可或缺的工具。

它的作用是在每次响应式状态发生改变时触发回调函数。这篇文章将带着5个疑问带你深入理解watch源码。

  • watch是如何监听响应式数据变化的
  • watch(source, cb, { immediate: true })为什么会立即执行一次 cb
  • watch默认是"浅监听",deep: true如何实现"深度监听"?
  • watch([refA, refB], cb)监听多个ref,是如何分别获取oldValuenewValue的?
  • 为什么job(调度任务)是watch的核心?

带着上面5个问题,我们进入源码中进行调试,看看watch是如何运行的,本文参考源码版本为"version": "3.5.13"

Demo调试

进入源码目录中的单元测试文件packages\reactivity__tests__\watch.spec.ts

ts 复制代码
describe('watch', () => {
...
​
  test('with callback', () => {
    let dummy: any
    const source = ref(0)
    // 此处断点
    watch(source, () => {
      dummy = source.value
    })
    expect(dummy).toBe(undefined)
    source.value++
    expect(dummy).toBe(1)
  })
...
}

vscode中运行调试,到断点这里进入watch源码文件packages\reactivity\src\watch.ts,我们先来看看watch函数是怎样实现的

watch函数

由于watch函数代码量比较多,我们拆开逻辑一部分一部分的去解读

ts 复制代码
​
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 reactiveGetter = (source: object) => {
    if (deep) return source
    if (isShallow(source) || deep === false || deep === 0)
      return traverse(source, 1)
    return traverse(source)
  }
​
  let effect: ReactiveEffect
  let getter: () => any
  let cleanup: (() => void) | undefined
  let boundCleanup: typeof onWatcherCleanup
  let forceTrigger = false
  let isMultiSource = false
​
  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source)
    forceTrigger = true
  } 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 {}
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = call
        ? () => call(source, WatchErrorCodes.WATCH_GETTER)
        : (source as () => any)
    } else {
      // no cb -> simple effect
      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
        }
      }
    }
  } else {
    getter = NOOP
  }
​
  if (cb && deep) {
    const baseGetter = getter
    const depth = deep === true ? Infinity : deep
    getter = () => traverse(baseGetter(), depth)
  }
​
  const scope = getCurrentScope()
  const watchHandle: WatchHandle = () => {
    effect.stop()
    if (scope && scope.active) {
      remove(scope.effects, effect)
    }
  }
​
  if (once && cb) {
    const _cb = cb
    cb = (...args) => {
      _cb(...args)
      watchHandle()
    }
  }
​
  let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
​
  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))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        const currentWatcher = activeWatcher
        activeWatcher = effect
        try {
          const args = [
            newValue,
            // pass undefined as the old value when it's changed for the first time
            oldValue === INITIAL_WATCHER_VALUE
              ? undefined
              : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
                ? []
                : oldValue,
            boundCleanup,
          ]
          call
            ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
            : // @ts-expect-error
              cb!(...args)
          oldValue = newValue
        } finally {
          activeWatcher = currentWatcher
        }
      }
    } else {
      // watchEffect
      effect.run()
    }
  }
​
  if (augmentJob) {
    augmentJob(job)
  }
​
  effect = new ReactiveEffect(getter)
​
  effect.scheduler = scheduler
    ? () => scheduler(job, false)
    : (job as EffectScheduler)
​
  boundCleanup = fn => onWatcherCleanup(fn, false, effect)
​
  cleanup = effect.onStop = () => {
    const cleanups = cleanupMap.get(effect)
    if (cleanups) {
      if (call) {
        call(cleanups, WatchErrorCodes.WATCH_CLEANUP)
      } else {
        for (const cleanup of cleanups) cleanup()
      }
      cleanupMap.delete(effect)
    }
  }
​
  // initial run
  if (cb) {
    if (immediate) {
      job(true)
    } else {
      oldValue = effect.run()
    }
  } else if (scheduler) {
    scheduler(job.bind(null, true), true)
  } else {
    effect.run()
  }
​
  watchHandle.pause = effect.pause.bind(effect)
  watchHandle.resume = effect.resume.bind(effect)
  watchHandle.stop = watchHandle
​
  return watchHandle
}

source数据处理

根据Vue3官方文档,source可以是ref、reactive、getter函数或者多个数据源组成的数组,我们先来看watch函数对source参数的处理:

ts 复制代码
...
  if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => reactiveGetter(source)
    forceTrigger = true
  } 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 {}
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = call
        ? () => call(source, WatchErrorCodes.WATCH_GETTER)
        : (source as () => any)
    } else {
      // no cb -> simple effect
      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
        }
      }
    }
  } else {
    getter = NOOP
  }
...

根据source数据类型进行以下处理,创建对应的getter函数

  • ref:监听 .value
  • reactive: 遍历对象,获取响应式属性
  • 数组:分别处理每个元素
  • 函数:如果有 cb,执行 source() 获取值,否则直接运行 source(),并支持 onCleanupwatchEffect

再往下看,到了deep判断部分,我们看看deep逻辑做了什么?

deep处理

watch默认是浅监听,为什么deep: true能深度监听?

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

这里判断cb && deep则执行traverse去递归自身进行深层遍历,traverse函数就是一个判断不同数据类型进行递归遍历自身的函数。

可能会有细心的同学发现,上面的source数据处理中,reactive对象的处理使用了reactiveGetter函数

ts 复制代码
  const reactiveGetter = (source: object) => {
    if (deep) return source
    if (isShallow(source) || deep === false || deep === 0)
      return traverse(source, 1)
    return traverse(source)
  }

在没传入deep时,手动执行traverse函数去遍历自身。如果是浅层reactive数据则遍历1次自身,非浅层reactive数据则深层递归自身。

但让人觉得很奇怪的一点是,为什么传入deep时只需要直接return source,而在没有传入deep时需要手动去执行traverse函数去遍历自身,这两者有什么区别呢?

带着疑问,我们接着往下看

ts 复制代码
... 
  const job = (immediateFirstRun?: boolean) => {
    ...
  }
​
  // 创建监听值的副作用实例
  effect = new ReactiveEffect(getter)
​
  effect.scheduler = scheduler
    ? () => scheduler(job, false)
    : (job as EffectScheduler)
...

这里创建effect副作用实例,以getter作为回调,并使用job作为调度器。

watch的核心在于job,它决定了cb何时执行,我们来看看job是如何工作的。

job函数

我们把job函数的代码简化一下

ts 复制代码
...
  const job = (immediateFirstRun?: boolean) => {
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      cb(newValue, oldValue)
      oldValue = newValue
    } else {
      // watchEffect
      effect.run()
    }
  }
...

这里判断cb是否存在,存在则执行effect.run,再去调用cbcb不存在时直接执行effect.run,类似watchEffect的实现方式

熟悉响应式原理的小伙伴一看就知道了,effect.run本质就是执行this.fn

ts 复制代码
...
run(): T {
  // 设置当前 effect
  activeSub = this
  shouldTrack = true
  return this.fn() // ✅ 这里才是依赖收集的真正入口
}
...

这里简写了run代码,不熟悉的小伙伴移步:Vue3源码解析之Ref、Effect

  • shouldTrack = true :告诉 Vue可以开始收集依赖

  • activeSub = this :记录当前 effect,当响应式数据 track() 时,知道应该把谁作为依赖收集。

  • 执行this.fn()

    这个fn()其实就是watchEffect(cb)watch(source)里的getter

    fn()里如果访问了响应式数据,就会触发track(),从而收集依赖。

那么上面的问题是不是一目了然了,sourcereactive对象时,deeptrue时直接return source,在effect.run这里去收集使用到 的属性。而traverse函数遍历自身,深层递归访问了所以属性,这里触发了MutableReactiveHandler类的get方法,track()了所有被访问到的属性。

MutableReactiveHandler类不熟悉的移步:手摸手带你阅读Vue3源码之Reactive 上

问题

还记得文章开始时我们提到的5个问题吗,到一步了我们看看解决了哪些

watch是如何监听响应式数据变化的
  • getter作为ReactiveEffectfn,这样effect.run执行时,getter会自动收集依赖
  • source变化时,Vue3 响应式系统trigger触发effect执行job
watch默认是"浅监听",deep: true 如何实现"深度监听"?
  • source不为reactive对象时,调用traverse函数遍历source进行依赖收集
  • sourcereactive对象时,直接return source,在effect.run里进行依赖收集
watch([refA, refB], cb)监听多个ref,是如何分别获取oldValuenewValue的?
  • 遍历source数组,对每个ref执行getter(),让它们收集依赖
  • cb(newValue, oldValue)依次传入最新值和旧值
为什么job(调度任务)是watch的核心?
  • job控制cb何时执行
  • 可配合传入scheduler进行优化,改变job执行时机
watch(source, cb, { immediate: true })为什么会立即执行一次 cb
  • watch方法后面初始化时判断了immediate: true立即执行一次job(true)
ts 复制代码
...
  // initial run
  if (cb) {
    if (immediate) {
      job(true)
    } else {
      oldValue = effect.run()
    }
  } else if (scheduler) {
    scheduler(job.bind(null, true), true)
  } else {
    effect.run()
  }
...

感谢阅读!欢迎点赞收藏关注,一键三连!!!

Vue3 源码解析系列

  1. 手摸手带你阅读Vue3源码之Reactive 上
  2. 手摸手带你阅读Vue3源码之Reactive 下
  3. Vue3源码解析之Ref、Effect
  4. Vue3源码解析之nextTick:拯救"数据变了但 DOM 还没反应过来"的尴尬场面
  5. Vue3源码:5个问题带你读懂watch
相关推荐
bin91531 小时前
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加导出数据功能示例9,TableView15_09带排序的导出表格示例
开发语言·前端·javascript·vue.js·ecmascript·deepseek
绿草在线3 小时前
pnpm创建vite
vue.js
KjPrime4 小时前
纯vue手写流程组件
前端·javascript·vue.js
化作繁星5 小时前
tailwindcss如何修改elementplus的内部样式
vue.js·elementplus·tailwind css
码农不惑6 小时前
前端开发:Vue以及Vue的路由
前端·javascript·vue.js
lina_mua6 小时前
JavaScript 中的性能优化:从基础到高级技巧
开发语言·javascript·性能优化
烛阴8 小时前
JavaScript instanceof:你真的懂它吗?
前端·javascript
shadouqi8 小时前
1.angular介绍
前端·javascript·angular.js
痴心阿文9 小时前
React如何导入md5,把密码password进行md5加密
前端·javascript·react.js