【Vue3源码学习】响应式 api: watchEffect | watch 的实现原理

在 Vue2中watchoption 写法中一个很常用的选项,使用它可以非常方便的监听一个数据源的变化,而在 Vue3 中watch 独立成了一个 响应式api。

源码地址:packages\runtime-core\src\apiWatch.ts

watchEffect

由于 watch中的许多行为都与 watchEffect 一致,所以 watchEffect 放在首位讲解,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在以来变更时重新运行该函数。

watchEffect 函数的实现非常简洁:

// 首先来看参数类型
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void

export interface WatchOptionsBase extends DebuggerOptions {
    
    
  flush?: 'pre' | 'post' | 'sync'
}

export type WatchStopHandle = () => void

export function watchEffect(
  effect: WatchEffect,  // 接收函数类型的变量,并且在这个函数中会传入 onInvalidate 参数,用以清除副作用
  options?: WatchOptionsBase // 在这个对象中有三个属性,你可以修改 flush 来改变副作用的刷新时机,默认为 pre,当修改为 post 时,就可以在组件更新后触发这个副作用侦听器,改同 sync 会强制同步触发。而 onTrack 和 onTrigger 选项可以用于调试侦听器的行为,并且两个参数只能在开发模式下工作。
): WatchStopHandle {
    
    
  return doWatch(effect, null, options)
}

其中 DebuggerOptions位于packages\reactivity\src\effect.ts

export interface DebuggerOptions {
    
    
  onTrack?: (event: DebuggerEvent) => void   // 追踪时触发
  onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
}

参数传入后,函数会执行并返回 doWatch 函数的返回值。由于 watch api 也会调用 doWatch 函数,所以 doWatch 函数的具体逻辑我们会放在最后看。先看 watch api 的函数实现。

watch

watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下这个侦听是惰性的,即只有当被侦听的源发生变化时才执行回调。

watchEffect 相比,watch 有以下不同:

  • 惰性地执行副作用;
  • 更具体地说明应触发侦听器重新运行的状态;
  • 访问被侦听状态的先前值和当前值。

watch 函数的函数有四次重载,这里不做具体分析,这里先看下watch的实现参数,后面将详细分析watch实现的核心doWatch

// overload: array of multiple sources + cb
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 接收 3 个参数,source 侦听的数据源,cb 回调函数,options 侦听选项。

source 参数

source 的类型如下:

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
type MultiWatchSources = (WatchSource<unknown> | object)[]

从两个类型定义看出,数据源支持传入单个的 RefComputed 响应式对象,或者传入一个返回相同泛型类型的函数,以及 source 支持传入数组,以便能同时监听多个数据源。

cb 参数

在这个最通用的声明中,cb 的类型是 any,但 cb 这个回调函数也有类型:

export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onInvalidate: InvalidateCbRegistrator
) => any

在回调函数中,会提供最新的 value、旧 value,以及 onInvalidate 函数用以清除副作用。

options

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
    
    
  immediate?: Immediate
  deep?: boolean
}

可以看到 options 的类型 WatchOptions 继承了 WatchOptionsBase

分析完参数后,可以看到函数体内的逻辑与 watchEffect 几乎一致,但是多了在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。执行 doWatch 时的传参与 watchEffect 相比,多了第二个参数回调函数。

doWatch

`watchEffect、watch 还是组件内的 watch 选项,在执行时最终调用的都是 doWatch 中的逻辑。

先从 doWatch 的函数签名看起,与 watch 一致。

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

doWatch 函数主要分为以下几个部分:

  1. 标准化 source,组装成为 getter 函数
  2. 组装 job 函数。判断侦听的值是否有变化,有变化则执行 getter 函数和 cb 回调
  3. 组装 scheduler 函数,scheduler 负责在合适的时机调用 job 函数(根据 options.flush,即副作用刷新的时机),默认在组件更新前执行
  4. 开启侦听
  5. 返回停止侦听函数
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {
    
     immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
    
    
 
  // 1. 根据 source 的类型组装 getter    
  let getter: () => any 
  if (isRef(source)) {
    
    
    getter = ...
  } else if (isReactive(source)) {
    
    
    getter = ...
  } else if (isArray(source)) {
    
    
    isMultiSource = true
    forceTrigger = source.some(isReactive) 
     getter = ...
  } else if (isFunction(source)) {
    
    
    if (cb) {
    
    
      getter = ...
    } else {
    
    
      getter = ...
    }
  } else {
    
    
    getter = ...
    __DEV__ && warnInvalidSource(source)
  }

  // 2. 组装 job
  const job: SchedulerJob = () => {
    
    
	// ...
  }

  // 3. 组装 scheduler
  let scheduler: EffectScheduler = ...

  // 4. 开启侦听,侦听的是 getter 函数
  const effect = new ReactiveEffect(getter, scheduler)
  effect.run()

  // 5. 返回停止侦听函数
  return () => {
    
    
    effect.stop()
    if (instance && instance.scope) {
    
    
      remove(instance.scope.effects!, effect)
    }
  }
}

1. 标准化 source,组装成为 getter 函数

判断 source 的类型,source 的不同类型,标准化包装成 getter 函数:

  • ref:() => source.value
  • reactive:() => traverse(source)
  • 数组:分别根据子元素类型,包装成 getter 函数
  • 函数:用 callWithErrorHandling 包装,实际上就是直接调用 source 函数

相关代码及注释如下:

  const instance = currentInstance // 取当前组件实例
  let getter: () => any            // getter 最终会当做副作用的函数参数传入
  let forceTrigger = false         // 标识是否需要强制更新
  let isMultiSource = false        // 标记传入的是单个数据源还是以数组形式传入的多个数据源。

  // 1. ref 类型 
  if (isRef(source)) {
    
    
    getter = () => source.value      // 直接解包取source.value 值,
    forceTrigger = !!source._shallow // 标记会根据是否是shallowRef设置
  // 2. reactive 类型
  } else if (isReactive(source)) {
    
    
    getter = () => source  // 直接返回 source,因为 reactive 的值不需要解包获取
    deep = true            // 由于 reactive 中往往有多个属性,所以将deep设置为 true。这里可以看出从外部给 reactive 设置 deep 是无效的
  // 3. 数组 array 类型
  } else if (isArray(source)) {
    
    
    isMultiSource = true
    forceTrigger = source.some(isReactive) // 会根据数组中是否存在 reactive 响应式对象来设置
    // 数组形式,由source内各个元素的单个 getter 结果
    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)
        }
      })
   // 4. source 是函数 function 类型
  } else if (isFunction(source)) {
    
    
    // 4.1 如果有回调函数
    if (cb) {
    
    
      // getter with cb
      // getter 就是 source 函数执行的结果,这种情况一般是 watch api 中的数据源以函数的形式传入。
      // callWithErrorHandling 中做了一些 vue 错误信息的统一处理,有更好的错误提示
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
    
    
      // no cb -> simple effect
      // 4.2 如果没有回调函数,那么此时就是 watchEffect api 的场景了
      getter = () => {
    
    
        // 如果组件实例已经卸载,则不执行,直接返回
        if (instance && instance.isUnmounted) {
    
    
          return
        }
        // cleanup不为void时 清除依赖
        if (cleanup) {
    
    
          cleanup()
        }
        // 执行source函数
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  // 5. 其余情况 将 getter 设置为空函数,并且报出 source 不合法的警告
  } else {
    
    
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

接着会处理 watch 中的场景,当有回调,并且 deep 选项为 true 时,将使用 traverse 来包裹getter 函数,对数据源中的每个属性递归遍历进行监听。

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

traverse

traverse 的作用:对于 reactive 对象或设置了参数 deep,需要侦听到深层次的变化,这需要深度遍历整个对象,深层次的访问其所有的响应式变量,并收集依赖。

// 深度遍历对象,只是访问响应式变量,不做任何处理
// 访问就会触发响应式变量的 getter,从而触发依赖收集
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
}

之后会声明 cleanuponInvalidate 函数,并在 onInvalidate 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用,这些响应需要在其失效时清除,所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作为入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时。
  • 侦听器被停止(如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)。
let cleanup: () => void
let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    
    
  cleanup = runner.options.onStop = () => {
    
    
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

2. 组装 job

Job 函数在 scheduler 函数中被直接或间接调用。

声明一个 job 函数,这个函数最终会作为调度器中的回调函数传入,由于是一个闭包形式依赖外部作用域中的许多变量。

根据是否有回调函数,设置 joballowRecurse 属性,这个设置很重要,能够让 job 作为一个观察者的回调这样调度器就能知道它允许调用自身。

  // 初始化 oldValue
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  // 声明一个 job 调度器任务
  const job: SchedulerJob = () => {
    
    
    if (!effect.active) {
    
     // 如果副作用以停用则直接返回
      return
    }
    if (cb) {
    
    
      // watch(source, cb)
      //  在 scheduler 中需要手动直接执行 effect.run,这里会执行 getter 函数
      // 先执行 getter 获取返回值,如果返回值变化,才执行 cb。
      const newValue = effect.run()
      
      // 判断是否需要执行 cb
      // 1. getter 函数的值被改变,没有发生改变则不执行 cb 回调
      // 2. 设置了 deep 深度监听
      // 3. forceTrigger 为 true
      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()
        }
        // 触发 watch api 的回调,并将 newValue、oldValue、onInvalidate 传入
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          // 首次调用时,将 oldValue 的值设置为 undefined
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue // 触发回调后,更新 oldValue
      }
    } else {
    
    
      // watchEffect的场景,直接执行 runner
      effect.run()
    }
  }
  // important: mark the job as a watcher callback so that scheduler knows
// 重要:让调度器任务作为侦听器的回调以至于调度器能知道它可以被允许自己派发更新
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb

3. 组装 scheduler

getter 中侦听的响应式变量发生改变时,就会执行 scheduler 函数。

声明一个 scheduler 的调度器对象,并根据 flush的传参来确定调度器的执行时机。这一部分逻辑的源码及注释如下:


let scheduler: ReactiveEffectOptions['scheduler'] // 声明一个调度器
if (flush === 'sync') {
    
     // 同步
  scheduler = job as any // 这个调度器函数会立即被执行
} else if (flush === 'post') {
    
     // 延迟
  // 调度器会将任务推入一个延迟执行的队列中,在组件被挂载后、更新的生命周期中执行。
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
    
    
    // 默认情况 'pre'
  scheduler = () => {
    
    
    // 区分组件是否已经挂载  
    if (!instance || instance.isMounted) {
    
    
      queuePreFlushCb(job) // 组件挂载后则会被推入优先执行时机的队列中
    } else {
    
    
      // 在 pre 选型中,第一次调用必须发生在组件挂载之前
      // 所以这次调用是同步的
      job()
    }
  }
}

4. 开启侦听

在处理完以上的调度器部分后,会开始创建const effect = new ReactiveEffect(getter, scheduler)对象,并首次执行副作用函数。这里会立即调用 getter 函数,进行依赖收集。

如果依赖有变化,则执行 scheduler 函数:

  • 如果 watch 有回调函数
    • 如果 watch 设置了 immediate 选项,则立即执行 job 调度器任务。
    • 否则首次执行 effect.run()副作用,并将返回值赋值给 oldValue
  • 如果 flush 的刷新时机是 post,则将 effect.run()放入延迟时机的队列中,等待组件挂载后执行。
  • 其余情况都直接首次执行effect.run()副作用。
  const effect = new ReactiveEffect(getter, scheduler)

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

  // initial run
  if (cb) {
    
    
    if (immediate) {
    
    
      job()  // 有回调函数且是 imeediate 选项的立即执行调度器任务
    } else {
    
    
      oldValue = effect.run() // 否则执行一次effect.run(),并将返回值赋值给 oldValue
    }
  } else if (flush === 'post') {
    
    
    // 如果调用时机为 post,则推入延迟执行队列
    queuePostRenderEffect(
      effect.run.bind(effect),
      instance && instance.suspense
    )
  } else {
    
    
    // 其余情况立即首次执行副作用
    effect.run()
  }

5. 返回停止监听函数

最后 doWatch 函数会返回一个函数,这个函数的作用是停止侦听,所以大家在使用时可以显式的为 watchwatchEffect 调用返回值以停止侦听。

// 返回一个函数,用以显式的结束侦听
return () => {
    
    
    effect.stop()
    // 移除当前组件上的对应的 effect
    if (instance && instance.scope) {
    
    
      remove(instance.scope.effects!, effect)
    }
}

doWatch 函数到这里就全部运行完毕了,现在所有的变量已经声明完毕。

结语

如果本文对你有一丁点帮助,点个赞支持一下吧,感谢感谢

猜你喜欢

转载自blog.csdn.net/qq_38987146/article/details/123211407