Vue3源码解析之 watch

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 5 篇,关注专栏

前言

Vue3 中响应式系统除了 reactive 和 ref 这两个函数外,我们还需了解下 computed 和 watch 这两个函数,它们也是响应式系统的关键所在,本篇我们来看下 watch 是如何实现的。

案例

首先引入 reactive、effect、watch 三个函数,之后声明 obj 响应式数据,接着执行 watch 函数,该函数第一个参为监听数据,第二个参为监听回调,最后两秒后又修改 obj 的 name 值。

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
        const { reactive, effect, watch } = Vue
        // 1. reactive 构建响应性数据
        const obj = reactive({
          name: 'jc'
        })

        // 2. 执行了 watch 函数
        watch(obj, (value, oldValue) => {
          console.log('watch 触发了')
          console.log(value)
        })

        // 3. 两秒后触发 setter 行为
        setTimeout(() => {
          obj.name = 'cc'
        }, 2000)
    </script>
  </body>
</html>

watch 实现

watch 函数定义在 packages/runtime-core/src/apiWatch.ts 文件下:

ts 复制代码
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  // 省略
  return doWatch(source as any, cb, options)
}

可以看出 watch 函数实际执行的是 doWatch 方法:

ts 复制代码
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
  // 省略

  const instance = currentInstance
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  if (isRef(source)) {
    // 是否 ref 类型
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // 是否 reactive 类型
    getter = () => source
    deep = 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 traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

  // 省略

  if (cb && deep) {
    const baseGetter = getter // getter 为 () => source
    getter = () => traverse(baseGetter())
  }

  // 省略
  
  // 定义 oldValue  isMultiSource 是否有多个源 [value1, value2] 需要监听
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  // job 核心逻辑
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }
  
  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb

  let scheduler: EffectScheduler
  if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    scheduler = () => queuePreFlushCb(job) // 调度器赋值  也是核心逻辑
  }

  const effect = new ReactiveEffect(getter, scheduler)

  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }

  // initial run
  if (cb) {
    if (immediate) {
      // 默认自动执行 watch 一次
      job() // job 触发意味着 watch 被立即执行一次
    } else {
      oldValue = effect.run() // 等于执行 fn 函数 即 () => traverse(baseGetter())  即 () => source 即 传入的监听数据
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    effect.run()
  }

  return () => {
    effect.stop() // 监听停止
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
}

根据传入 source 监听数据类型不同走不同逻辑,当前 source 为 reactive 类型,所以 getter 直接赋值为 () => source。另外还可以看到类型为 reactive 时,默认开启深度监听 deep = true 。由于存在 cb 监听回调和 deep,所以baseGetter 等于 getter ,即 () => source, getter 赋值为 () => traverse(baseGetter()) 。

之后又定义了 oldValue 值,默认为空对象,也是回调函数中的 oldValue。接着定义了一个 job 函数,这是 watch 的核心逻辑 ,我们稍后再来分析。然后又创建了一个调度器 scheduler ,我们在 computed 文中也提到过,在依赖触发时,会执行该方法。此时 scheduler 被赋值为 () => queuePreFlushCb(job),将 job 函数传入到 queuePreFlushCb 方法中,该逻辑之后来分析。

接着又创建了一个 ReactiveEffect 实例,将赋值后的 getter 和 scheduler 传入,ReactiveEffect 的作用之前文章也提到过,这里不再具体说明。

由于存在 cb 回调函数,根据判断配置中 immediate 存在时,就执行 job 方法,我们可以理解为 job 的触发 watch 被立即执行一次。否则执行 effect.run 即执行 fn 方法,当前 fn 为 getter 即 () => traverse(baseGetter()),就是执行 () => source,结果为传入的监听对象 source :

最后 oldValue 赋值:

此时 watch 函数执行完毕,两秒后触发 obj 的 setter 行为,依赖触发 trigger 执行,再看下当前 effects :

之后再遍历执行每个 effect,此时存在 scheduler 调度器,执行 scheduler 方法。当前 scheduler 为 之前赋值的 () => queuePreFlushCb(job),我们再来看下 queuePreFlushCb 方法,该方法定义在 packages/runtime-core/src/scheduler.ts 文件中:

ts 复制代码
export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

实际执行的是 queueCb 方法:

ts 复制代码
function queueCb(
  cb: SchedulerJobs,
  activeQueue: SchedulerJob[] | null,
  pendingQueue: SchedulerJob[],
  index: number
) {
  if (!isArray(cb)) {
    if (
      !activeQueue ||
      !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
    ) {
      pendingQueue.push(cb)
    }
  } else {
    // if cb is an array, it is a component lifecycle hook which can only be
    // triggered by a job, which is already deduped in the main queue, so
    // we can skip duplicate check here to improve perf
    pendingQueue.push(...cb)
  }
  queueFlush()
}

该方法定义了一个 pendingQueue 队列数组,插入传入的 cb 回调即传入的 job 函数,然后执行 queueFlush 方法:

ts 复制代码
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

可以看出 watch 的 job 执行都是一个 微任务。当前同步任务执行完毕后,执行微任务 ,之后执行 flushJobs 方法:

ts 复制代码
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }

  flushPreFlushCbs(seen)

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  queue.sort((a, b) => getId(a) - getId(b))

  // conditional usage of checkRecursiveUpdate must be determined out of
  // try ... catch block since Rollup by default de-optimizes treeshaking
  // inside try-catch. This can leave all warning code unshaked. Although
  // they would get eventually shaken by a minifier like terser, some minifiers
  // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        // console.log(`running:`, job.id)
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0

    flushPostFlushCbs(seen)

    isFlushing = false
    currentFlushPromise = null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

然后执行 flushPreFlushCbs(seen) 方法:

ts 复制代码
export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob // job 函数
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 取代 pendingPreFlushCbs
    pendingPreFlushCbs.length = 0 // 置空 下次不会再触发
    if (__DEV__) {
      seen = seen || new Map()
    }
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      activePreFlushCbs[preFlushIndex]() // 当前 job 函数执行
    }
    activePreFlushCbs = null
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // recursively flush until it drains
    flushPreFlushCbs(seen, parentJob)
  }
}

当前 pendingPreFlushCbs 为传入的 job 方法,之后将去重后的 pendingPreFlushCbs 赋值给 activePreFlushCbs,遍历执行 activePreFlushCbs[preFlushIndex](),实际是执行每个 job 函数,我们再回过来看下 job 函数:

ts 复制代码
const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }

run 方法执行实际执行 getter 即 () => traverse(baseGetter()) ,此时 newValue :

这里还得再看下 traverse 方法:

ts 复制代码
export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

该方法由于值类型不同,会递归处理返回最终的值。接着执行 callWithAsyncErrorHandling 方法:

ts 复制代码
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] {
  if (isFunction(fn)) {
    const res = callWithErrorHandling(fn, instance, type, args)
    if (res && isPromise(res)) {
      res.catch(err => {
        handleError(err, instance, type)
      })
    }
    return res
  }

  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}

export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  // 统一处理监听 错误
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

可以看出 callWithErrorHandling 这里执行了 cb 回调函数即 watch 传入的匿名函数,callWithAsyncErrorHandling 主要是对错误统一监听处理。最后将 newValue 赋值给 oldValue,watch 至此执行完毕。

总结

  1. watch 函数实际执行的是 doWatch 方法。
  2. 调度器 scheduler 在 watch 中很关键。
  3. scheduler 、ReactiveEffect 两者之间存在互相作用的关系,一旦 effect 触发了 scheduler ,那么会导致 queuePreFlushCb(job) 执行,job 方法就被塞入微任务的队列中。
  4. 只要 job() 触发,那么就表示 watch 触发了一次。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
相关推荐
计算机魔术师1 小时前
DeepSeek 据报道接近完成至少 800 亿元融资,腾讯与宁德时代参与
前端
可乐ea1 小时前
多模型编排的三层:框架、模型路由与提供商路由
前端·网络·人工智能·ai智能体·多智能体协作·多模型编排·大模型路由
vx-程序开发3 小时前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业
青柠之夏cc3 小时前
前端拖拽功能原生实现,不引入拖拽库完成业务
开发语言·前端·python
ym hyd 1113 小时前
题库建立与考试系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·vue.js·spring boot·毕设
liangshanbo12153 小时前
前端大文件切片上传怎么做?
前端
Sand(ContextGate)4 小时前
Python Agent 测试实战:测试与评估,让 Agent 像传统软件一样可交付
前端·javascript·python·microsoft·ai
广州华水科技4 小时前
单北斗GNSS变形监测系统在城市安全与地质灾害中的应用前景
前端
颜进强4 小时前
20 · NestJs循环依赖与 forwardRef:容器为什么在环面前会死,"占位再补齐"怎么救
前端·后端·ai编程
颜进强4 小时前
19 · NestJs @Global 落地账:装饰器与 `isGlobal` 参数,各在什么场景上岗
前端·后端·ai编程