ホームページ  >  記事  >  ウェブフロントエンド  >  Vue3リスナーウォッチの実装原理は何ですか

Vue3リスナーウォッチの実装原理は何ですか

WBOY
WBOY転載
2023-06-04 14:05:112371ブラウズ

ウォッチの本質

いわゆるウォッチ、その本質は、応答データを監視し、データが変化したときに対応するコールバック関数を通知して実行することです。実際、watch 実装 の本質は、effect および options.scheduler オプションを使用することです。次の例に示すように:

// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数
function watch(source, cb){
  effect(
    // 触发读取操作,从而建立联系
  	() => source.foo,
    {
      scheduler(){
        // 当数据变化时,调用回调函数 cb
        cb()
      }
    }
  )
}

コードに示すように、source は応答データであり、cb はコールバック関数です。副作用関数にスケジューラ オプションがある場合、応答データが変更されると、副作用関数の実行が直接トリガーされるのではなく、スケジューラー関数の実行がトリガーされます。この観点から見ると、スケジューラのスケジューリング機能はコールバック関数に相当し、watch の実装ではこれを利用しています。

watch の関数シグネチャ

複数のソースのリッスン

次の関数シグネチャに示すように、リッスンするデータ ソースは配列にすることができます:

// packages/runtime-core/src/apiWatch.ts

// 数据源是一个数组
// 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

次の関数シグネチャに示すように、配列を使用して複数のソースを同時にリッスンすることもできます。

// packages/runtime-core/src/apiWatch.ts

// 使用数组同时侦听多个源
// 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

単一ソースをリッスンする

リッスンするデータ ソース次の関数シグネチャに示すように、ref 型データであるか、戻り値を持つゲッター関数です。

// packages/runtime-core/src/apiWatch.ts

// 数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数
// 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

export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)

次の関数シグネチャに示すように、リスニング データ ソースは応答型の obj オブジェクトです:

// packages/runtime-core/src/apiWatch.ts

// 数据源是一个响应式的 obj 对象
// 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 実装

watch function

// packages/runtime-core/src/apiWatch.ts

// 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 つのパラメータ、つまりソース リスニング データ ソース、cb コールバック関数、オプション リスニング オプションを受け取ります。

ソース パラメータ

watch の関数オーバーロードから、単一のソースをリッスンする場合、ソースは ref 型データまたは戻り値を持つゲッター関数であることがわかります。または、応答性の高い obj オブジェクトにすることもできます。複数のソースを聴く場合、ソースは配列にすることができます。

cb パラメータ

cb コールバック関数では、最新の値、古い値、および副作用を除去するための onCleanup 関数が開発者に提供されます。次の型定義に示すように:

export type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onCleanup: OnCleanup
) => any

options パラメータ

options オプションは監視の動作を制御できます。たとえば、オプションの直後のオプション パラメータを使用して、コールバックを実行するかどうかを制御できます。オプション パラメータは、監視コールバック関数が同期的に実行されるか非同期的に実行されるかを制御します。 options パラメータの型定義は次のとおりです:

export interface WatchOptionsBase extends DebuggerOptions {
  flush?: &#39;pre&#39; | &#39;post&#39; | &#39;sync&#39;
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}

オプションの型定義が表示されます WatchOptions は WatchOptionsBase を継承します。つまり、watch のオプションに含まれる 2 つの固有のパラメーター (immediate と deep) に加えて、WatchOptionsBase のすべてのパラメーターを渡して、副作用の実行の動作を制御することもできます。

doWatch 関数は watch の関数本体で呼び出されますので、その実装を見てみましょう。

doWatch 関数

実際、watch 関数であっても watchEffect 関数であっても、実行中に最終的に doWatch 関数が呼び出されます。

doWatch 関数シグネチャ

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

doWatch の関数シグネチャは基本的に watch の関数シグネチャと同じで、3 つのパラメータも受け取ります。 options オプションを使いやすくするために、doWatch 関数はそれを分解します。

変数の初期化

まず、component を通じて現在のコンポーネント インスタンスを取得し、次に 3 つの異なる変数を宣言します。関数の 1 つはゲッターと呼ばれ、引数として副作用関数に渡されます。変数forceTriggerは、副作用関数を強制する必要があるかどうかを示すブール値です。 isMultiSource 変数もブール値で、リスニング データ ソースが単一のソースであるか、配列形式で渡された複数のソースであるかをマークするために使用されます。初期値は false で、リスニング データ ソースが単一のソースであることを示します。次のコードに示すように:

  const instance = currentInstance
  let getter: () => any
  // 是否需要强制触发副作用函数执行   
  let forceTrigger = false
  // 侦听的是否是多个源
  let isMultiSource = false

次に、リスニング データ ソースに従ってこれら 3 つの変数を初期化します。

リスニング データ ソースが ref 型データである

リスニング データ ソースが ref 型データの場合、source.value Getter を返すことで初期化します。つまり、 、getter 関数がトリガーされると、実際のリスニング データは、source.value を通じて取得されます。次に、isShallow 関数を使用して、リッスンしたデータ ソースが浅い応答であるかどうかを判断し、その結果を ForceTrigger に代入して、forceTrigger 変数の初期化を完了します。次のコードに示すように:

if (isRef(source)) {
  // 侦听的数据源是 ref
  getter = () => source.value
  // 判断数据源是否是浅响应
  forceTrigger = isShallow(source)
}

リスニング データ ソースが応答データである

リスニング データ ソースが応答データの場合、ソースを直接返して初期化します。つまり、ゲッター関数がトリガーされると、リスニング データ ソースを直接返します。応答データはオブジェクトオブジェクトである可能性があるため、deep を true に設定すると、ゲッター関数がトリガーされたときにオブジェクトの属性値を再帰的に読み取ることができます。次のコードに示すように:

else if (isReactive(source)) {
  // 侦听的数据源是响应式数据
  getter = () => source
  deep = true
}

リスニング データ ソースは配列です

リスニング データ ソースが配列の場合、つまり、複数のリスニング データ ソースが同時にソースを聴きました。この時点で、isMultiSource 変数を直接 true に設定し、複数のソースがリッスンされていることを示します。次に、配列の some メソッドを使用して、複数のリスニング ソースに応答するオブジェクトがあるかどうかを検出し、その結果を ForceTrigger に割り当てます。配列を走査し、各ソースのタイプに基づいてゲッター関数の初期化を完了します。次のコードに示すように:

else if (isArray(source)) {
  // 侦听的数据源是一个数组,即同时侦听多个源
  isMultiSource = true
  forceTrigger = source.some(isReactive)
  getter = () =>
    // 遍历数组,判断每个源的类型 
    source.map(s => {
      if (isRef(s)) {
        // 侦听的数据源是 ref  
        return s.value
      } else if (isReactive(s)) {
        // 侦听的数据源是响应式数据 
        return traverse(s)
      } else if (isFunction(s)) {
        // 侦听的数据源是一个具有返回值的 getter 函数 
        return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      } else {
        __DEV__ && warnInvalidSource(s)
      }
    })
}

リスニング データ ソースは関数です

当侦听的数据源是一个具有返回值的 getter 函数时,判断 doWatch 函数的第二个参数 cb 是否有传入。如果有传入,则处理的是 watch 函数的场景,此时执行 source 函数,将执行结果赋值给 getter 。该情况仅适用于 watchEffect 函数未接收到参数的情况。如果组件实例已被卸载,则直接返回而不执行 source 函数,根据该场景进行处理。如果未能执行成功,则执行清除依赖的代码并调用source函数,将返回结果赋值给getter。如下面的代码所示:

else if (isFunction(source)) {

  // 处理 watch 和 watchEffect 的场景
  // watch 的第二个参数可以是一个具有返回值的 getter 参数,第二个参数是一个回调函数
  // watchEffect 的参数是一个 函数

  // 侦听的数据源是一个具有返回值的 getter 函数 
  if (cb) {
    // getter with cb
    // 处理的是 watch 的场景
    // 执行 source 函数,将执行结果赋值给 getter   
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    // no cb -> simple effect
    // 没有回调,即为 watchEffect 的场景  
    getter = () => {
      // 件实例已经卸载,则不执行,直接返回
      if (instance && instance.isUnmounted) {
        return
      }
      // 清除依赖
      if (cleanup) {
        cleanup()
      }
      // 执行 source 函数
      return callWithAsyncErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onCleanup]
      )
    }
  }
}

递归读取响应式数据

如果侦听的数据源是一个响应式数据,需要递归读取响应式数据中的属性值。如下面的代码所示:

// 处理的是 watch 的场景
// 递归读取对象的属性值  
if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter())
}

在上面的代码中,doWatch 函数的第二个参数 cb 有传入,说明处理的是 watch 中的场景。deep 变量为 true ,说明此时侦听的数据源是一个响应式数据,因此需要调用 traverse 函数来递归读取数据源中的每个属性,对其进行监听,从而当任意属性发生变化时都能够触发回调函数执行。

定义清除副作用函数

声明 cleanup 和 onCleanup 函数,并在 onCleanup 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用时,这些响应需要在其失效是清除。如下面的代码所示:

// 清除副作用函数
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
  cleanup = effect.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

封装 scheduler 调度函数

为了便于控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数,如下面的代码所示:

// 将 scheduler 调度函数封装为一个独立的 job 函数,便于在初始化和变更时执行它
const job: SchedulerJob = () => {
  if (!effect.active) {
    return
  }
  if (cb) {
    // 处理 watch 的场景 
    // watch(source, 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))
    ) {
      
      // 当回调再次执行前先清除副作用
      // cleanup before running cb again
      if (cleanup) {
        cleanup()
      }

      // 执行watch 函数的回调函数 cb,将旧值和新值作为回调函数的参数
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        newValue,
        
        // 首次调用时,将 oldValue 的值设置为 undefined
        // pass undefined as the old value when it&#39;s changed for the first time
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
        onCleanup
      ])
      // 更新旧值,不然下一次会得到错误的旧值
      oldValue = newValue
    }
  } else {
    // watchEffect
    // 处理 watchEffect 的场景
    effect.run()
  }
}

在 job 函数中,判断回调函数 cb 是否传入,如果有传入,那么是 watch 函数被调用的场景,否则就是 watchEffect 函数被调用的场景。

如果是 watch 函数被调用的场景,首先执行副作用函数,将执行结果赋值给 newValue 变量,作为最新的值。然后判断需要执行回调函数 cb 的情况:

  • 如果侦听的数据源是响应式数据,需要深度侦听,即 deep 为 true

  • 如果需要强制触发副作用函数执行,即 forceTrigger 为 true

  • 如果新旧值发生了变化

如果存在上述三种情况之一,就必须执行 watch 函数的回调函数 cb。如果回调函数 cb 是再次执行,在执行之前需要先清除副作用。然后调用 callWithAsyncErrorHandling 函数执行回调函数cb,并将新值newValue 和旧值 oldValue 传入回调函数cb中。在回调函数cb执行后,更新旧值oldValue,避免在下一次执行回调函数cb时获取到错误的旧值。

如果是 watchEffect 函数被调用的场景,则直接执行副作用函数即可。

设置 job 的 allowRecurse 属性

设置 job 函数的 allowRecurse 属性根据是否传递回调函数 cb 来进行。这个设置非常关键,因为它可以使作业充当监听器的回调,这样调度程序就能够知道它是否允许调用自身。

// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
// 重要:让调度器任务作为侦听器的回调以至于调度器能知道它可以被允许自己派发更新
job.allowRecurse = !!cb

flush 选项指定回调函数的执行时机

在调用 watch 函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:

  • 当 flush 的值为 sync 时,代表调度器函数是同步执行,此时直接将 job 赋值给 scheduler,这样调度器函数就会直接执行。

  • 当 flush 的值为 post 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行。

  • 当 flush 的值为 pre 时,即调度器函数默认的执行方式,这时调度器会区分组件是否已经挂载。如果组件未挂载,则先执行一次调度函数,即执行回调函数cb。在组件挂载之后,将调度函数推入一个优先执行时机的队列中。

    // 这里处理的是回调函数的执行时机
    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' // 调度器函数默认的执行模式 scheduler = () => { if (!instance || instance.isMounted) { // 组件挂载后将 job 推入一个优先执行时机的队列中 queuePreFlushCb(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. // 在 pre 选型中,第一次调用必须发生在组件挂载之前 // 所以这次调用是同步的 job() } } }

创建副作用函数

初始化完 getter 函数和调度器函数 scheduler 后,调用 ReactiveEffect 类来创建一个副作用函数

// 创建一个副作用函数
const effect = new ReactiveEffect(getter, scheduler)

执行副作用函数

在执行副作用函数之前,首先判断是否传入了回调函数cb,如果有传入,则根据 options 的 immediate 选项来判断是否需要立即执行回调函数cb,如果指定了immediate 选项,则立即执行 job 函数,即 watch 的回调函数会在 watch 创建时立即执行一次。如果不这样做,就需要手动调用副作用函数,将其返回值赋值给oldValue作为旧值。如下面的代码所示:

if (cb) {
  // 选项参数 immediate 来指定回调是否需要立即执行
  if (immediate) {
    // 回调函数会在 watch 创建时立即执行一次
    job()
  } else {
    // 手动调用副作用函数,拿到的就是旧值
    oldValue = effect.run()
  }
}

如果 options 的 flush 选项的值为 post ,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数。如下面的代码所示:

else if (flush === &#39;post&#39;) {
  // 在调度器函数中判断 flush 是否为 &#39;post&#39;,如果是,将其放到微任务队列中执行
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense
  )
}

其余情况都是立即执行副作用函数。如下面的代码所示:

else {
  // 其余情况立即首次执行副作用
  effect.run()
}

返回匿名函数,停止侦听

最终,doWatch函数返回了一个匿名函数,该函数用于取消对数据源的监听。因此在调用 watch 或者 watchEffect 时,可以调用其返回值类结束侦听。

return () => {
  effect.stop()
  if (instance && instance.scope) {
    // 返回一个函数,用以显式的结束侦听
    remove(instance.scope.effects!, effect)
  }
}

以上がVue3リスナーウォッチの実装原理は何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。