
💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》
1、前言
所谓 watch,其本质就是观测一个响应式数据,当数据发生变化的时通知并执行相应的回调函数。如下示例:
            
            
              javascript
              
              
            
          
          watch(obj, () => {
   console.log("数据变化了")
}))
        假设 watch是一个响应数据,使用 watch函数观测它,并传递一个回调函数,当修改响应式数据 obj 的值的时候,会触发该回调函数执行。
实际上,watch的实质本质上就是利用了 effect以及 options.scheduler选项,如下代码所示:
            
            
              javascript
              
              
            
          
          effect(() => {
  console.log(obj.foo)
}, {
scheduler() {
  // 当 obj.foo 的值变化时,就会执行scheduler调度函数
 }
})
        如果一个副作用函数存在 scheduler选项,当响应式数据发生变化时,会触发 scheduler调度函数执行,而非直接触发副作用函数执行。从这个角度来看,其实 scheduler调度函数就相当于一个回调函数,而 watch的实现就是利用了这个特性。以下是最简单的 watch函数实现:
            
            
              javascript
              
              
            
          
          function watch(source, cb) {
  // 触发读取操作,从而建立联系
  effect(() => source.foo,
    {
    scheduler() {
      // 当数据变化时,调用回调函数cb
      cb()
    }
  })
}
        2、watch 源码实现
watch函数定义在:packages/runtime-core/src/apiWatch.ts文件下。
2.1 函数签名
            
            
              javascript
              
              
            
          
          export function watch<
  T extends MultiWatchSources,
  Immediate extends Readonly<boolean> = false
>(
  sources: [...T],
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
  T extends Readonly<MultiWatchSources>,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: watching reactive object w/ cb
export function watch<
  T extends object,
  Immediate extends Readonly<boolean> = false
>(
  source: T,
  cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
  options?: WatchOptions<Immediate>
): WatchStopHandle
        从以上源码中,我们可以发现,watch函数可以监听的数据源有:
- 侦听的数据源是一个 
ref类型的数据 或者是一个具有返回值的getter函数; - 侦听的数据源是一个数组;
 
2.2 watch 源码实现
无论哪种函数签名,在 watch函数实现中的源码如下:
            
            
              javascript
              
              
            
          
          // implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}
        在该函数中,watch 函数接收了 3 个参数,分别是:source 侦听的数据源,cb 回调函数,options 侦听选项。watch 函数最终都调用了 doWatch函数。
2.2.1 source 参数
从定义可知,source 可以是一个 ref 类型的数据,或者是一个具有返回值的 getter 函数,也可以是一个响应式的 obj 对象。当侦听的是多个源时,source 可以是一个数组。
2.2.2 cb 参数
cb 参数的定义如下:
            
            
              javascript
              
              
            
          
          export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onCleanup: OnCleanup
) => any
        value:最新的值;oldValue: 更新前的值;onCleanup:用于清除副作用。
2.2.3 options 参数
options 参数的类型定义如下:
            
            
              javascript
              
              
            
          
          export interface WatchOptionsBase extends DebuggerOptions {
  flush?: 'pre' | 'post' | 'sync'
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}
        - immediate:控制 watch 的回调是否立即执行;
 - deep:控制 watch 的监听是否是深度的;
 - flush:调整回调函数的刷新时机;
 
2.3 doWatch 函数实现
doWatch 的函数与 watch 函数的签名基本一致,也是接收三个参数。在 doWatch 函数中,为了便于 options 选项的使用,对 options 进行了解构。
            
            
              javascript
              
              
            
          
          function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle
        doWatch函数实现很长,下面只贴出我们需要理解的关键部分。
2.3.1 标准化 source
通过前文我们知道,source可以是 getter函数,也可以是响应式对象甚至是响应式对象数组,因此,我们需要根据传入的 source,生成标准化的 getter函数,处理流程如下:
            
            
              javascript
              
              
            
          
          if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    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)
  }
        标准化 source的处理流程拆解如下:
- 如果 
source是ref对象,则创建一个访问source.value的getter函数; - 如果 
source是source对象,则创建一个访问source的getter函数,并设置deep为true; - 如果 
source是一个函数,则会继续判断第二个参数cb是否存在,然后对 cb 函数做简单的封装处理; - 如果 
source是一个数组,则内部会通过source.map函数映射处一个新的数组,它会判断每个数组元素的类型,映射规则与前面的source规则一致; - 如果 
source不满足上述条件,则在非生产环境下发出警告,提示source类型不合法; 
2.3.2 创建 job
处理完 watch函数的第一个参数 source后,接下来处理第二个参数 cb。
我们知道,cb 是一个回调函数,其拥有三个参数:
newValue: 代表新值;oldValue:代表旧值;onInvalidate:表示注册无效的回调函数;
那么如何判断值是否发生了变化,如何计算和存储旧值和新值呢?
我们可以在内部创建一个 job,它是对 cb回调函数做的一层封装,维护新值旧值的计算和存储,以及是否需要执行回调函数,当侦听的值发生变化时就会执行 job。
            
            
              javascript
              
              
            
          
          // 注册无效回调函数
let onCleanup: OnCleanup = (fn: () => void) => {
  cleanup = effect.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    cleanup = effect.onStop = undefined
  }
}
// 旧值初始值
let oldValue: any = isMultiSource
  ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
  : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
  if (!effect.active) {
    return
  }
  if (cb) {
    // 新值
    const newValue = effect.run()
    // 满足执行回调函数的条件
    if (
      deep ||
      forceTrigger ||
      (isMultiSource
        ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
        : hasChanged(newValue, oldValue)) ||
      (__COMPAT__ &&
        isArray(newValue) &&
        isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
    ) {
      // cleanup before running cb again
      // 执行清理函数
      if (cleanup) {
        cleanup()
      }
      // 执行回调函数 cb
      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
          : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
        onCleanup
      ])
      // 更新旧值
      oldValue = newValue
    }
  } else {
    // watchEffect
    effect.run()
  }
}
// 允许触发自身
job.allowRecurse = !!cb
        创建 job函数的处理流程如下:
- 判断回调函数 
cb是否传入,如果有传入,那么是watch的调用场景,否则是watchEffect函数被调用的场景;- 如果是 
watch函数被调用的场景,首先执行副作用函数获取最新的值newValue,然后判断是否需要执行回调函数cb的情况:- 监听的数据是 
reactive类型,即 deep 的值为 true; - 需要强制执行副作用函数,即 
forceTrigger为 true; - 新旧值发生了变化; 如果满足上面条件中的一个,那么先清除副作用函数,然后调用 
callWithAsyncErrorHandling函数,将新旧值newValue和oldValue传入该函数中,执行完毕后更新旧值 oldValue,避免在下一次执行回调函数cb时获取到错误的旧值。 
 - 监听的数据是 
 - 如果是 
watchEffect函数被调用的场景,则直接执行副作用函数即可; - 设置 job 的 allowRecurse 属性,它能够让 job 作为侦听器的回调,这样调度器就能知道它允许调用自身。
 
 - 如果是 
 
2.3.3 创建 scheduler
当调用 watch函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:
flush: sync,代表它是一个同步的watcher,即数据变化时同步执行回调函数;flush: post,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行;flush: pre,即调度器函数默认的执行方式,在组件更新之前执行,如果组件还没有挂载,则在组件挂载之前同步执行回调函数。
            
            
              javascript
              
              
            
          
          let scheduler: EffectScheduler
  if (flush === 'sync') {
    // 同步执行,将job赋值给调度器
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    // 将调度函数job添加到微任务队列中执行
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }
        2.3.4 创建副作用 effect
初始化 getter函数和调度函数 scheduler后,调用 ReactiveEffect来创建一个副作用函数。然后经过以下步骤处理:
            
            
              javascript
              
              
            
          
          const effect = new ReactiveEffect(getter, scheduler)
  if (__DEV__) {
    effect.onTrack = onTrack
    effect.onTrigger = onTrigger
  }
  // initial run
  // 初次执行
  if (cb) {
    // 选项参数 immediate 来指定回调是否需要立即执行
    if (immediate) {
      // 手动调用副作用函数,拿到的就是旧值
      job()
    } else {
      // 求旧值
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    // 没有cb并且flush不为post的情况
    effect.run()
  }
        - 判断传入的回调函数 
cb是否存在,如果存在,则根据传入的options选项中immediate,如果是否为 true,则会在创建watch的时候立即执行一次,否则,否则就手动调用副作用函数,并将返回值作为旧值,赋值给oldValue; - 如果 
options的flush的选项的值为post,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数; - 其余情况就是立即执行副作用函数。
 
2.3.5 返回销毁函数
最后,会返回销毁函数,也就是 watch执行后返回的函数,我们可以通过调用它来停止 watcher对数据的监听,如下代码所示:
            
            
              javascript
              
              
            
          
          const unwatch = () => {
  effect.stop()
  if (instance && instance.scope) {
    // 移除组件effects对这个effect的引用
    remove(instance.scope.effects!, effect)
  }
}
        销毁函数内部会执行 effect.stop()函数让 effect失效,并清理 effect的相关依赖,这样就可以停止对数据的监听。同时,如果是组件中注册的 watcher,也会移除组件 effects对这个 effect的引用。
3、总结
watch本质上利用了副作用函数重新执行时的可调度性。一个 watch本身会创建一个 effect,当这个 effect依赖的响应式数据发生变化时,会执行该 effect的调度函数,即 scheduler。
watch可以侦听单一数据源,也可以侦听多个源。单一的数据源可以是具有返回值的 getter函数,或者一个 ref对象,也可以是 reactive对象。
watch可以通过 flush指定回调函数和副作用函数的执行时机,可指定的参数值有 post、sync、pre(默认)。
watch可以通过指定 immediate为 true,这样 watch在创建的时候会立即执行一次回调函数。
4、参考资料
1\][vue官网](https://link.juejin.cn?target=https%3A%2F%2Fcn.vuejs.org%2F "https://cn.vuejs.org/") \[2\][vuejs设计与实现](https://link.juejin.cn?target=https%3A%2F%2Fwww.ituring.com.cn%2Fbook%2F2953 "https://www.ituring.com.cn/book/2953") \[3\][vue3源码](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fcore%2Fblob%2Fv3.3.4 "https://github.com/vuejs/core/blob/v3.3.4")