关于前端:vue3源码五watch源码解析

4次阅读

共计 12460 个字符,预计需要花费 32 分钟才能阅读完成。

【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}
)

// or
watch(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.value
    forceTrigger = isShallow(source)
    }
  • 如果 sourcereactive类型,getter是个返回 source 的函数,并将 deep 设置为true

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

    if (isArray(source)) {
    isMultiSource = true
    forceTrigger = 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: () => void
let 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_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 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: EffectScheduler
if (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 = 2

state.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 = 2

state.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 的函数。

正文完
 0