当前位置 : 主页 > 网络编程 > JavaScript >

Vue3源码解析watch函数实例

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 引言 一、watch参数类型 1. 选项options 2. 回调cb 3. 数据源source 二、watch函数 三、watch的核心:doWatch 函数 引言 想起上次面试,问了个古老的问题: watch和computed的区别 。多少有点感
目录
  • 引言
  • 一、watch参数类型
    • 1. 选项options
    • 2. 回调cb
    • 3. 数据源source
  • 二、watch函数
    • 三、watch的核心:doWatch 函数

      引言

      想起上次面试,问了个古老的问题:watch和computed的区别。多少有点感慨,现在已经很少见这种耳熟能详的问题了,网络上八股文不少。今天,我更想分享一下从源码的层面来区别这八竿子打不着的两者。本篇针对watch做分析,下一篇分析computed

      一、watch参数类型

      我们知道,vue3里的watch接收三个参数:侦听的数据源source、回调cb、以及可选的optiions

      1. 选项options

      我们可以在options里根据需要设置**immediate来控制是否立即执行一次回调;设置deep来控制是否进行深度侦听;设置flush来控制回调的触发时机,默认为{ flush: 'pre' },即vue组件更新前;若设置为{ flush: 'post' }则回调将在vue组件更新之后触发;此外还可以设置为{ flush: 'sync' },表示同步触发;以及设置收集依赖时的onTrack和触发更新时的onTrigger两个listener,主要用于debuggerwatch函数会返回一个watchStopHandle用于停止侦听。options**的类型便是WatchOptions,在源码中的声明如下:

      // reactivity/src/effect.ts
      export interface DebuggerOptions {
        onTrack?: (event: DebuggerEvent) => void
        onTrigger?: (event: DebuggerEvent) => void
      }
      ​
      // runtime-core/apiWatch.ts
      export interface WatchOptionsBase extends DebuggerOptions {
        flush?: 'pre' | 'post' | 'sync'
      }
      ​
      export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
        immediate?: Immediate
        deep?: boolean
      }
      

      2. 回调cb

      了解完options,接下来我们看看回调**cb**。通常我们的cb接收三个参数:valueoldValueonCleanUp,然后执行我们需要的操作,比如侦听表格的页码,发生变化时重新请求数据。第三个参数onCleanUp,用于注册副作用清理的回调函数, 在副作用下次执行之前,这个回调函数会被调用,通常用来清除不需要的或者无效的副作用。

      // 副作用
      export type WatchEffect = (onCleanup: OnCleanup) => void
      ​
      export type WatchCallback<V = any, OV = any> = (
        value: V,
        oldValue: OV,
        onCleanup: OnCleanup
      ) => any
      ​
      type OnCleanup = (cleanupFn: () => void) => void
      

      3. 数据源source

      watch函数可以侦听单个数据或者多个数据,共有四种重载,对应四种类型的source。其中,单个数据源的类型有WatchSource和响应式的object,多个数据源的类型为MultiWatchSourcesReadonly<MultiWatchSources>,而MultiWatchSources其实也就是由单个数据源组成的数组。

      // 单数据源类型:可以是 Ref 或 ComputedRef 或 函数
      export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
      ​
      // 多数据源类型
      type MultiWatchSources = (WatchSource<unknown> | object)[]
      ​
      

      二、watch函数

      下面是源码中的类型声明,以及watch的重载签名和实现签名:

      // watch的重载与实现
      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
      ​
      // 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的实现签名中可以看到,和watchEffect不同,watch的第二个参数cb必须是函数,否则会警告。最后,尾调用了doWatch,那么具体的实现细节就都得看doWatch了。让我们来瞅瞅它到底是何方神圣。

      三、watch的核心:doWatch 函数

      先瞄一下doWatch的签名:接收的参数大体和watch一致,其中source里多了个WatchEffect类型,这是由于在watchApi.js文件里,还导出了三个函数:watchEffectwatchSyncEffectwatchPostEffect,它们接收的第一个参数的类型就是WatchEffect,然后传递给doWatch,会在后面讲到,也可能不会;而options默认值为空对象,函数返回一个WatchStopHandle,用于停止侦听。

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

      再来看看doWatch的函数体,了解一下它干了些啥:

      首先是判断在没有cb的情况下,如果options里设置了immediatedeep,就会告警,这俩属性只对有cbdoWatch签名有效。其实也就是上面说到的watchEffect等三个函数,它们是没有cb这个参数的,因此它们设置的immediatedeep是无效的。声明一个当source参数不合法时的警告函数,代码如下:

      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.`
            )
          }
        }
      ​
      // 声明一个source参数不合法的警告函数
      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.`
          )
        }
      // ...
      

      接下来,就到了正文了。第一步的目标是设置getter,顺便配置一下强制触发和深层侦听等。拿到getter的目的是为了之后创建effectvue3的响应式离不开effect,日后再出一篇文章介绍。

      先拿到当前实例,声明了空的getter,初始化关闭强制触发,且默认为单数据源的侦听,然后根据传入的source的类型,做不同的处理:

      • Ref: getter返回值为Ref的·value,强制触发由source是否为浅层的Ref决定;
      • Reactive响应式对象:getter的返回值为source本身,且设置深层侦听;
      • Arraysource为数组,则是多数据源侦听,将isMultiSource设置为true,强制触发由数组中是否存在Reactive响应式对象或者浅层的Ref来决定;并且设置getter的返回值为从source映射而来的新数组;
      • function:当source为函数时,会判断有无cb,有cb则是watch,否则是watchEffect等。当有cb时,使用callWithErrorHandling包裹一层来调用source得到的结果,作为getter的返回值;
      • otherTypes:其它类型,则告警source参数不合法,且getter设置为NOOP,一个空的函数。
      // 拿到当前实例,声明了空的getter,初始化关闭强制触发,且默认为单数据源的侦听
      const instance = currentInstance
      let getter: () => any
      let forceTrigger = false
      let isMultiSource = false
      ​
      // 根据侦听数据源的类型做相应的处理
      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 = () =>
            // 可见,数组成员只能是Ref、Reactive或者函数,其它类型无法通过校验,将引发告警
            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)
        }
      

      然后还顺便兼容了下vue2.x版本的watch

      // 2.x array mutation watch compat
        if (__COMPAT__ && cb && !deep) {
          const baseGetter = getter
          getter = () => {
            const val = baseGetter()
            if (
              isArray(val) &&
              checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
            ) {
              traverse(val)
            }
            return val
          }
        }
      

      然后判断了下deepcb,在深度侦听且有cb的情况下(说白了就是watch而不是watchEffect等),对getter做个traverse,该函数的作用是对getter的返回值做一个递归遍历,将遍历到的值添加到一个叫做seen的集合中,seen的成员即为当前watch要侦听的那些数据。代码如下(影响主线可先跳过):

      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)
        // Ref
        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
      }
      

      至此,getter就设置好了。之后声明了cleanuponCleanup,用于清除副作用。以及SSR检测。虽然不是本文的重点,但还是贴一下源码:

      let cleanup: () => void
        let onCleanup: OnCleanup = (fn: () => void) => {
          cleanup = effect.onStop = () => {
            callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
          }
        }
      // in SSR there is no need to setup an actual effect, and it should be noop
      // unless it's eager
      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
      }
      

      随后就是重头戏了,拿到oldValue,以及在job函数中取得newValue,这不就是我们在使用watch的时候的熟悉套路嘛。

      let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
      // job为当前watch要做的工作,后续通过调度器来处理
      const job: SchedulerJob = () => {
        // 当前effect不在active状态,说明没有触发该effect的响应式变化,直接返回 
        if (!effect.active) {
          return
        }
        // cb存在,说明是watch,而不是watchEffect
        if (cb) {
          // watch(source, cb)
          // 调用 effect.run 得到新的值 newValue
          const newValue = effect.run()
          if (
            deep ||
            forceTrigger ||
            // 取到的新值和旧值是否相同,如果有变化则进入分支
            (isMultiSource
              ? (newValue as any[]).some((v, i) =>
                  hasChanged(v, (oldValue as any[])[i])
                )
              : hasChanged(newValue, oldValue)) ||
            // 兼容2.x
            (__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 : oldValue,
              onCleanup
            ])
            // cb执行完成,当前的新值就变成了旧值
            oldValue = newValue
          }
        } else {
          // cb不存在,则是watchEffect
          // watchEffect
          effect.run()
        }
      }
      // 设置allowRecurse,让调度器知道它可以自己触发
      job.allowRecurse = !!cb
      

      一看job里,在watch的分支出现了effect,但是这个分支并没有effect呀,再往下看,噢,原来是由之前取得的getter来创建的effect。在这之前,还定义了调度器,调度器scheduler被糅合进了effect里,影响了newValue的获取,从而影响cb的调用时机:

      • sync:同步执行,也就是回调cb直接执行;
      • pre:默认值是pre,表示组件更新前执行;
      • post:组件更新后执行。
      let scheduler: EffectScheduler
      // 根据flush的值来创建不同的调度器
      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)
      }
      // 为 watch 创建 effect ,watchEffect就不必了,因为自带的有
      const effect = new ReactiveEffect(getter, scheduler)
      // 主要是调试用的onTrack和onTrigger,当收集依赖和触发更新时做一些操作
      if (__DEV__) {
        effect.onTrack = onTrack
        effect.onTrigger = onTrigger
      }
      

      现在来到了doWatch最后的环节了:侦听器的初始化。

      • immediate:如果为真值。将直接调用一次job,上文我们知道,job是包裹了一层错误处理程序来调用cb,所以我们现在终于亲眼看到了为什么immediate能让cb立即触发一次。
      // initial run
      // 有cb,是 watch
      if (cb) {
        if (immediate) {
          job()
        } else {
          // 获取一下当前的值作为旧值
          oldValue = effect.run()
        }
      } else if (flush === 'post') {
        // 没有cb,是watchEffect,副作用的时机在组件更新之后,用queuePostRenderEffect包裹一层来调整时机
        queuePostRenderEffect(
          effect.run.bind(effect),
          instance && instance.suspense
        )
      } else {
        // watchEffect,副作用的时机在组件更新之前,直接执行一次effect.run
        effect.run()
      }
      // 返回一个WatchStopHandle,内部执行 effect.stop来达到停止侦听的作用
      return () => {
        effect.stop()
        // 移除当前实例作用域下的当前effect
        if (instance && instance.scope) {
          remove(instance.scope.effects!, effect)
        }
      }
      

      到这里,watch的源码算是差不多结束了。小结一下核心流程:

      • watch:判断若没有cb则告警;
      • watch:尾调用doWatch,之后的操作都在doWatch里进行;
      • doWatch:判断没有cb时若设置了deepimmediate则告警;
      • doWatch:根据source的类型得到getter
      • doWatch:如果cb存在且deep为真则对getter()进行递归遍历;
      • doWatch:获取oldValue,声明job函数,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb
      • doWatch:根据post的值定义的调度器scheduler
      • doWatch:根据getterscheduler创建effect
      • doWatch:初始化侦听器,如果有cbimmediate为真值,则立即调用job函数,相当于调用我们写的cb;如果immediate为假值,则只调用effect.run()来初始化oldValue
      • doWatch:返回一个WatchStopHandle,内部通过effect.stop()来实现停止侦听。
      • watch:接收到doWatch返回的WatchStopHandle,并返回给外部使用。

      以上就是Vue3源码解析watch函数实例的详细内容,更多关于Vue3 watch函数的资料请关注自由互联其它相关文章!

      上一篇:如何让别人访问本地运行的vue项目
      下一篇:没有了
      网友评论