Rumah > hujung hadapan web > View.js > Apakah prinsip pelaksanaan jam tangan pendengar Vue3

Apakah prinsip pelaksanaan jam tangan pendengar Vue3

WBOY
Lepaskan: 2023-06-04 14:05:11
ke hadapan
2439 orang telah melayarinya

Intipati jam tangan

Apa yang dipanggil jam tangan, intinya adalah untuk memerhati data responsif dan memberitahu serta melaksanakan fungsi panggil balik yang sepadan apabila data berubah. Malah, intipati pelaksanaan jam tangan adalah menggunakan kesan dan pilihan.pilihan penjadual. Seperti yang ditunjukkan dalam contoh berikut:

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

Seperti yang ditunjukkan dalam kod, sumber ialah data reaktif dan cb ialah fungsi panggil balik. Jika terdapat pilihan penjadual dalam fungsi kesan sampingan, apabila data responsif berubah, pelaksanaan fungsi penjadual akan dicetuskan dan bukannya mencetuskan secara langsung pelaksanaan fungsi kesan sampingan. Dari perspektif ini, fungsi penjadualan penjadual adalah bersamaan dengan fungsi panggil balik, dan pelaksanaan jam tangan mengambil kesempatan daripada ini.

Tandatangan fungsi jam tangan

Mendengar berbilang sumber

Sumber data pendengaran boleh berupa tatasusunan, seperti yang ditunjukkan dalam tandatangan fungsi berikut:

rreee

Anda juga boleh menggunakan tatasusunan untuk mendengar berbilang sumber pada masa yang sama, seperti yang ditunjukkan dalam tandatangan fungsi berikut:

// 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
Salin selepas log masuk

Dengar satu sumber

Sumber data untuk mendengar ialah ref jenis data atau ialah fungsi pengambil dengan nilai pulangan, seperti yang ditunjukkan dalam tandatangan fungsi berikut:

// 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
Salin selepas log masuk

Sumber data pendengaran ialah objek obj responsif, seperti yang ditunjukkan dalam tandatangan fungsi berikut:

// 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)
Salin selepas log masuk

Pelaksanaan jam tangan

fungsi jam tangan

// 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
Salin selepas log masuk

Anda dapat melihat bahawa fungsi jam tangan menerima 3 parameter, iaitu: sumber data mendengar sumber, fungsi panggil balik cb, pilihan mendengar pilihan.

parameter sumber

Daripada kelebihan beban fungsi jam tangan, kita boleh tahu bahawa apabila mendengar sumber tunggal, sumber boleh menjadi data jenis ref atau fungsi pengambil dengan nilai pulangan, atau ia boleh menjadi objek obj responsif. Apabila mendengar berbilang sumber, sumber boleh menjadi tatasusunan.

parameter cb

Dalam fungsi panggil balik cb, ia menyediakan pembangun dengan nilai terkini, nilai lama dan fungsi onCleanup untuk membersihkan kesan sampingan. Seperti yang ditunjukkan dalam definisi jenis berikut:

// 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)
}
Salin selepas log masuk

parameter pilihan

pilihan pilihan boleh mengawal gelagat jam tangan Contohnya, melalui parameter pilihan serta-merta pilihan, anda boleh mengawal sama ada panggilan balik daripada jam tangan dilaksanakan serta-merta Parameter pilihan mengawal sama ada fungsi panggil balik jam tangan dilaksanakan secara serentak atau tidak segerak. Takrif jenis parameter pilihan adalah seperti berikut:

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

Anda boleh melihat definisi jenis pilihan WatchOptions mewarisi WatchOptionsBase. Maksudnya, sebagai tambahan kepada dua parameter unik iaitu pilihan jam tangan serta-merta dan dalam, semua parameter dalam WatchOptionsBase juga boleh diluluskan untuk mengawal tingkah laku pelaksanaan kesan sampingan.

Fungsi doWatch dipanggil dalam badan fungsi jam tangan. Mari kita lihat pelaksanaannya.

fungsi doWatch

Malah, sama ada fungsi jam tangan atau fungsi watchEffect, fungsi doWatch akhirnya dipanggil semasa pelaksanaan.

Tandatangan fungsi doWatch

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
}
Salin selepas log masuk

Tandatangan fungsi doWatch pada asasnya adalah sama dengan tandatangan fungsi jam tangan, dan juga menerima tiga parameter. Untuk memudahkan penggunaan pilihan pilihan, fungsi doWatch menyahbinanya.

Memulakan pembolehubah

Mula-mula, dapatkan contoh komponen semasa melalui komponen, dan kemudian isytiharkan tiga pembolehubah berbeza. Salah satu fungsi dipanggil pengambil, yang dihantar sebagai hujah kepada fungsi kesan sampingan. ForceTrigger berubah ialah nilai Boolean yang menunjukkan sama ada fungsi kesan sampingan perlu dipaksa. Pembolehubah isMultiSource juga merupakan nilai Boolean, digunakan untuk menandakan sama ada sumber data mendengar ialah satu sumber atau berbilang sumber yang dihantar dalam bentuk tatasusunan Nilai awal adalah palsu, menunjukkan bahawa sumber data mendengar ialah satu sumber. Seperti yang ditunjukkan dalam kod berikut:

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

Seterusnya, ketiga-tiga pembolehubah ini dimulakan berdasarkan sumber data yang didengar.

Sumber data pendengaran ialah data jenis ref

Apabila sumber data pendengaran ialah data jenis ref, mulakan dengan mengembalikan source.value Getter, iaitu , apabila fungsi getter dicetuskan, data pendengaran sebenar akan diperoleh melalui source.value. Kemudian gunakan fungsi isShallow untuk menentukan sama ada sumber data yang didengar adalah tindak balas cetek, dan tetapkan hasilnya kepada forceTrigger untuk melengkapkan permulaan pembolehubah forceTrigger. Seperti yang ditunjukkan dalam kod berikut:

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

Sumber data mendengar ialah data responsif

Apabila sumber data mendengar ialah data responsif, terus Kembalikan sumber untuk memulakan pengambil, iaitu, apabila fungsi pengambil dicetuskan, ia secara langsung mengembalikan sumber data pendengaran. Memandangkan data responsif mungkin objek, tetapkan dalam kepada benar, dan nilai atribut objek boleh dibaca secara rekursif apabila fungsi getter dicetuskan. Seperti yang ditunjukkan dalam kod berikut:

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

Sumber data pendengaran ialah tatasusunan

Apabila sumber data pendengaran ialah tatasusunan, iaitu, berbilang pendengar didengari pada masa yang sama sumber. Pada masa ini, tetapkan terus pembolehubah isMultiSource kepada benar, menunjukkan bahawa berbilang sumber sedang didengari. Kemudian gunakan beberapa kaedah tatasusunan untuk mengesan sama ada terdapat objek responsif dalam berbilang sumber pendengaran dan tetapkan hasilnya kepada forceTrigger. Lintas tatasusunan dan lengkapkan pemulaan fungsi getter berdasarkan jenis setiap sumber. Seperti yang ditunjukkan dalam kod berikut:

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

Sumber data pendengaran ialah fungsi

当侦听的数据源是一个具有返回值的 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]
      )
    }
  }
}
Salin selepas log masuk

递归读取响应式数据

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

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

在上面的代码中,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)
  }
}
Salin selepas log masuk

封装 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()
  }
}
Salin selepas log masuk

在 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
Salin selepas log masuk

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)
Salin selepas log masuk

执行副作用函数

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

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

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

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

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

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

返回匿名函数,停止侦听

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

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

Atas ialah kandungan terperinci Apakah prinsip pelaksanaan jam tangan pendengar Vue3. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:yisu.com
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan