【vue3源码】五、watch源码解析

参考代码版本:vue 3.2.37

官网文档:https://vuejs.org/

watch用来监听特定数据源,并在独自的回调函数中执行副作用。默认是惰性的——即回调仅在侦听源发生变化时被调用。
文件地位:packages/runtime-core/src/apiWatch.ts

应用示例

监听一个getter函数:

const state = reactive({ count: 0 })watch(  () => state.count,  (newVal, oldVal) => {    //...   })

监听一个ref

const count = ref(0)watch(  count,  (newVal, oldVal) => {    //...   })

监听多个数据源:

const foo = ref('')const bar = ref('')watch(  [ foo, bar ],  ([ newFoo, newBar ], [ oldFoo, oldBar ]) => {    // ...  })

深度监听:

const state = reactive({ count: 0 })watch(  () => state,  () => {    // ...  },  { deep: true })// orwatch(state, () => {  // ...})

源码剖析

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接管三个参数:source监听的源、cb回调函数、options监听配置,watch函数返回一个进行监听函数。。

watch中调用了一个叫做doWatch的函数,与watch作用类似的watchEffectwatchPostEffectwatchSyncEffect外部也都应用了这个doWatch函数。

export function watchEffect(  effect: WatchEffect,  options?: WatchOptionsBase): WatchStopHandle {  return doWatch(effect, null, options)}export function watchPostEffect(  effect: WatchEffect,  options?: DebuggerOptions) {  return doWatch(    effect,    null,    (__DEV__      ? Object.assign(options || {}, { flush: 'post' })      : { flush: 'post' }) as WatchOptionsBase  )}export function watchSyncEffect(  effect: WatchEffect,  options?: DebuggerOptions) {  return doWatch(    effect,    null,    (__DEV__      ? Object.assign(options || {}, { flush: 'sync' })      : { flush: 'sync' }) as WatchOptionsBase  )}

可见doWatchwatch API的外围,接下来重点钻研doWatch的实现。

doWatch

doWatch源码过长,这里就不搬运了,在剖析过程中,会展现相干代码。

doWatch函数接管三个参数:source监听的数据源,cb回调函数,options:监听配置。doWatch返回一个进行监听函数。

function doWatch(  source: WatchSource | WatchSource[] | WatchEffect | object,  cb: WatchCallback | null,  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ): WatchStopHandle {  // ...}

首先须要对immediatedeep做校验,如果cbnullimmediatedeep不为undefined进行提醒。

if (__DEV__ && !cb) {  if (immediate !== undefined) {    warn(      `watch() "immediate" option is only respected when using the ` +        `watch(source, callback, options?) signature.`    )  }  if (deep !== undefined) {    warn(      `watch() "deep" option is only respected when using the ` +        `watch(source, callback, options?) signature.`    )  }}

紧接着申明了一些变量:

const warnInvalidSource = (s: unknown) => {  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.`  )}// 以后组件实例const instance = currentInstance// 副作用函数,在初始化effect时应用let getter: () => any// 强制触发监听let forceTrigger = false// 是否为多数据源。let isMultiSource = false

而后依据传入的soure确定getterforceTriggerisMultiSource。这里分了5个分支:

  • 如果sourceref类型,getter是个返回source.value的函数,forceTrigger取决于source是否是浅层响应式。

    if (isRef(source)) {getter = () => source.valueforceTrigger = isShallow(source)}
  • 如果sourcereactive类型,getter是个返回source的函数,并将deep设置为true

    if (isReactive(source)) {getter = () => sourcedeep = true}
  • 如果source是个数组,将isMultiSource设为trueforceTrigger取决于source是否有reactive类型的数据,getter函数中会遍历source,针对不同类型的source做不同解决。

    if (isArray(source)) {isMultiSource = trueforceTrigger = source.some(isReactive)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)    }  })}
  • 如果source是个function。存在cb的状况下,getter函数中会执行source,这里source会通过callWithErrorHandling函数执行,在callWithErrorHandling中会解决source执行过程中呈现的谬误;不存在cb的话,在getter中,如果组件曾经被卸载了,间接return,否则判断cleanupcleanup是在watchEffect中通过onCleanup注册的清理函数),如果存在cleanup执行cleanup,接着执行source,并返回执行后果。source会被callWithAsyncErrorHandling包装,该函数作用会解决source执行过程中呈现的谬误,与callWithErrorHandling不同的是,callWithAsyncErrorHandling会解决异步谬误。

    if (isFunction(source)) {if (cb) {  getter = () =>    callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)} else {  // watchEffect  getter = () => {    // 如果组件实例曾经卸载,间接return    if (instance && instance.isUnmounted) {      return    }    // 如果清理函数,则执行清理函数    if (cleanup) {      cleanup()    }    // 执行source,传入onCleanup,用来注册清理函数    return callWithAsyncErrorHandling(      source,      instance,      ErrorCodes.WATCH_CALLBACK,      [onCleanup]    )  }}}

callWithErrorHandling函数能够接管四个参数:fn待执行的函数、instance组件实例、typefn执行过程中呈现的谬误类型、argsfn执行所需的参数。

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}

callWithAsyncErrorHandling的参数与callWithErrorHandling相似,与callWithErrorHandling不同的是,callWithAsyncErrorHandling能够承受一个fn数组。

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}
  • 其余状况,getter会被赋为一个空函数

    getter = NOOP__DEV__ && warnInvalidSource(source)

接下来会对vue2的数组的进行兼容性解决,breaking-changes/watch

if (__COMPAT__ && cb && !deep) {  const baseGetter = getter  getter = () => {    const val = baseGetter()    if (      isArray(val) &&      checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)    ) {      traverse(val)    }    return val  }}

如果存在cb并且deeptrue,那么须要对数据进行深度监听,这时,会从新对getter赋值,在新的getter函数中递归拜访之前getter的返回后果。

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

traverse实现,递归遍历所有属性,seen用于避免循环援用问题。

export function traverse(value: unknown, seen?: Set<unknown>) {  // 如果value不是对象或value不可被转为代理(通过markRaw解决),间接return value  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {    return value  }  // sean用于暂存拜访过的属性,防止出现循环援用的问题  // 如:  // const obj = { a: 1 }  // obj.b = obj  seen = seen || new Set()  // 如果seen中曾经存在了value,意味着value中存在循环援用的状况,这时return value  if (seen.has(value)) {    return value  }  // 增加value到seen中  seen.add(value)  // 如果是ref,递归拜访value.value  if (isRef(value)) {    traverse(value.value, seen)  } else if (isArray(value)) { // 如果是数组,遍历数组并调用traverse递归拜访元素内的属性    for (let i = 0; i < value.length; i++) {      traverse(value[i], seen)    }  } else if (isSet(value) || isMap(value)) { // 如果是Set或Map,调用traverse递归拜访汇合中的值    value.forEach((v: any) => {      traverse(v, seen)    })  } else if (isPlainObject(value)) { // 如果是原始对象,调用traverse递归方位value中的属性    for (const key in value) {      traverse((value as any)[key], seen)    }  }  // 最初须要返回value  return value}

到此,getter函数(getter函数中会尽可能拜访响应式数据,尤其是deeptrue并存在cb的状况时,会调用traverse实现对source的递归属性拜访)、forceTriggerisMultiSource曾经被确定,接下来申明了两个变量:cleanuponCleanuponCleanup会作为参数传递给watchEffect中的effect函数。当onCleanup执行时,会将他的参数通过callWithErrorHandling封装赋给cleanupeffect.onStopeffect在后文中创立)。

let cleanup: () => voidlet onCleanup: OnCleanup = (fn: () => void) => {  cleanup = effect.onStop = () => {    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)  }}

紧接着是一段SSR处理过程:

if (__SSR__ && isInSSRComponentSetup) {  // we will also not call the invalidate callback (+ runner is not set up)  onCleanup = NOOP  if (!cb) {    getter()  } else if (immediate) {    callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [      getter(),      isMultiSource ? [] : undefined,      onCleanup    ])  }  return NOOP}

而后申明了一个oldValuejob变量。如果是多数据源oldValue是个数组,否则是个对象。

job函数的作用是触发cb(watch)或执行effect.run(watchEffect)。job函数中会首先判断effect的激活状态,如果未激活,则return。而后判断如果存在cb,调用effet.run获取最新值,下一步就是触发cb,这里触发cb须要满足以下条件的任意一个条件即可:

  1. 深度监听deep===true
  2. 强制触发forceTrigger===true
  3. 如果多数据源,newValue中存在与oldValue中的值不雷同的项(利用Object.is判断);如果不是多数据源,newValueoldValue不雷同。
  4. 开启了vue2兼容模式,并且newValue是个数组,并且开启了WATCH_ARRAY

只有合乎上述条件的任意一条,便可已触发cb,在触发cb之前会先调用cleanup函数。执行完cb后,须要将newValue赋值给oldValue

如果不存在cb,那么间接调用effect.run即可。

let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUEconst 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 as any[])[i])        )        : hasChanged(newValue, oldValue)) ||      (__COMPAT__ &&        isArray(newValue) &&        isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))    ) {      if (cleanup) {        cleanup()      }      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [        newValue,        // 如果oldValue为INITIAL_WATCHER_VALUE,阐明是第一次watch,那么oldValue是undefined        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,        onCleanup      ])      oldValue = newValue    }  } else {    effect.run()  }}job.allowRecurse = !!cb

接下来申明了一个调度器scheduler,在scheduler中会依据flush的不同决定job的触发机会:

let scheduler: EffectSchedulerif (flush === 'sync') {  scheduler = job as any } else if (flush === 'post') {  // 提早执行,将job增加到一个提早队列,这个队列会在组件挂在后、更新的生命周期中执行  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)} else {  // 默认 pre,将job增加到一个优先执行队列,该队列在挂载前执行  scheduler = () => {    if (!instance || instance.isMounted) {      queuePreFlushCb(job)    } else {      job()    }  }}

此时,getterscheduler筹备实现,创立effect实例。

const effect = new ReactiveEffect(getter, scheduler)

创立effect实例后,开始首次执行副作用函数。这里针对不同状况有多个分支:

  • 如果存在cb的状况

    • 如果immediatetrue,执行job,触发cb
    • 否则执行effect.run()进行依赖的收集,并将后果赋值给oldValue
  • 如果flush===post,会将effect.run推入一个提早队列中
  • 其余状况,也就是watchEffect,则会执行effect.run进行依赖的收集
if (cb) {  if (immediate) {    job()  } else {    oldValue = effect.run()  }} else if (flush === 'post') {  queuePostRenderEffect(    effect.run.bind(effect),    instance && instance.suspense  )} else {  effect.run()}

最初,返回一个函数,这个函数的作用是进行watch对数据源的监听。在函数外部调用effect.stop()effect置为失活状态,如果存在组件实例,并且组件示例中存在effectScope,那么须要将effecteffectScope中移除。

return () => {  effect.stop()  if (instance && instance.scope) {    remove(instance.scope.effects!, effect)  }}

watchEffect、watchSyncEffect、watchPostEffect

watchEffectwatchSyncEffectwatchPostEffect的实现均是通过doWatch实现。

export function watchEffect(  effect: WatchEffect,  options?: WatchOptionsBase): WatchStopHandle {  return doWatch(effect, null, options)}export function watchPostEffect(  effect: WatchEffect,  options?: DebuggerOptions) {  return doWatch(    effect,    null,    (__DEV__      ? Object.assign(options || {}, { flush: 'post' })      : { flush: 'post' }) as WatchOptionsBase  )}export function watchSyncEffect(  effect: WatchEffect,  options?: DebuggerOptions) {  return doWatch(    effect,    null,    (__DEV__      ? Object.assign(options || {}, { flush: 'sync' })      : { flush: 'sync' }) as WatchOptionsBase  )}

watch与watchEffect的区别

watch只会追踪在source中明确的数据源,不会追踪回调函数中拜访到的货色。而且只在数据源发生变化后触发回调。watch会防止在产生副作用时追踪依赖(当产生副作用时,会执行调度器,在调度器中会将job推入不同的工作队列,达到管制回调函数的触发机会的目标),因而,咱们能更加准确地管制回调函数的触发机会。

watchEffect,会在副作用产生期间追踪依赖。它会在同步执行过程中,主动追踪所有能拜访到的响应式property

示例剖析

为了更好地了解watchwatchEffect的流程,咱们以上面几个例子来了解watchwatchEffect

例1

const state = reactive({ str: 'foo', obj: { num: 1 } })const flag = ref(true)watch(  [ flag, () => state.obj ],  ([ newFlag, newObj ], [ oldFlag, oldObj ]) => {    console.log(newFlag)    console.log(newObj.num)    console.log(oldFlag)    console.log(oldObj && oldObj.num)  },  {    immediate: true,    flush: 'sync'  })state.obj.num = 2state.obj = {  num: 2}

watch中调用doWatch办法,在doWatch会结构getter函数,因为所监听的数据源是个数组,所以getter函数返回值也是个数组,因为数据源的第一项是个ref,所以getter返回值第一项是ref.value,数据源的第二项是个function,所以getter返回值第二项是() => state.obj的返回值,也就是state.obj,因为咱们未指定depp,最终生成的getter() => [ref.value, state.obj]

而后利用getterscheduler生成effect,因为咱们指定了immediate: true,所以会立刻执行job函数,在job函数中,会执行effect.run()(这个过程中最终执行getter函数,而在执行getter函数的过程中会被对应响应式对象的proxy所拦挡,进而收集依赖),而后将effect.run()的后果赋值给newValue。而后对位比拟newValueoldValue中的元素,因为oldValue此时是个空数组,所以会触发cb,在cb触发过程中将newValueoldValue顺次传入,此时打印true 1 undefined undefined,当cb执行完,将newValue赋值为oldValue

当执行state.obj.num = 2时。因为在上一次的依赖收集过程中(也就是getter执行过程中),并没有拜访到num属性,也就不会收集它的依赖,所以该步骤不会影响到watch

state.obj = { num: 2 }时,会触发到obj对应的依赖,而在依赖触发过程中会执行调度器,因为flushsync,所以调度器就是job,当执行job时,通过effect.run()失去newValue,因为这时oldValue中的state.valuenewValue中的state.value曾经不是同一个对象了,所以触发cb。打印true 2 true 2

为什么第二次打印newObj.numoldObj.num雷同?因为oldValue中的oldObj保留的是state.obj的援用地址,一旦state.obj产生扭转,oldValue也会对应扭转。

例2

const state = reactive({ str: 'foo', obj: { num: 1 } })const flag = ref(true)watchEffect(() => {  console.log(flag.value)  console.log(state.obj.num)})state.obj.num = 2state.obj = {  num: 3}

与例1雷同,例2学生成gettergetter中会调用source)与scheduler,而后生成effect。因为watchEffect是没有cb参数,也未指定flush,所以会间接执行effct.run()。在effect.run执行过程中,会调用source,在source执行过程中会将effect收集到flag.deptargetMap[toRaw(state)].objtargetMap[toRaw(state).obj].num中。所以第一次打印true 1

当执行state.obj.num = 2,会触发targetMap[toRaw(state).obj].num中的依赖,也就是effect,在触发依赖过程中会执行effect.scheduler,将job推入一个pendingPreFlushCbs队列中。

当执行state.obj = { num: 3 },会触发targetMap[toRaw(state)].obj中的依赖,也就是effect,在触发依赖过程中会执行effect.scheduler,将job推入一个pendingPreFlushCbs队列中。

最初会执行pendingPreFlushCbs队列中的job,在执行之前会对pendingPreFlushCbs进行去重,也就是说最初只会执行一个job。最终打印true 3

总结

watchwatchEffectwatchSyncEffectwatchPostEffect的实现均是通过一个doWatch函数实现。

dowatch中会首先生成一个getter函数。如果是watchAPI,那么这个getter函数中会依据传入参数,拜访监听数据源中的属性(可能会递归拜访对象中的属性,取决于deep),并返回与数据源数据类型统一的数据(如果数据源是ref类型,getter函数返回ref.value;如果数据源类型是reactivegetter函数返回值也是reactive;如果数据源是数组,那么getter函数返回值也应该是数组;如果数据源是函数类型,那么getter函数返回值是数据源的返回值)。如果是watchEffect等API,那么getter函数中会执行source函数。

而后定义一个job函数。如果是watchjob函数中会执行effect.run获取新的值,并比拟新旧值,是否执行cb;如果是watchEffect等API,job中执行effect.run。那么如何只监听到state.obj.num的变换呢?

当申明完job,会紧跟着定义一个调度器,这个调度器的作用是依据flushjob放到不同的工作队列中。

而后依据getter调度器scheduler初始化一个ReactiveEffect`实例。

接着进行初始化:如果是watch,如果是立刻执行,则马上执行job,否则执行effect.run更新oldValue;如果flushpost,会将effect.run函数放到提早队列中提早执行;其余状况执行effect.run

最初返回一个进行watch的函数。