欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3源碼解析watch函數(shù)實例

 更新時間:2022年10月21日 15:55:42   作者:ChrisLey  
這篇文章主要為大家介紹了Vue3源碼解析watch函數(shù)實例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

引言

想起上次面試,問了個古老的問題:watch和computed的區(qū)別。多少有點感慨,現(xiàn)在已經(jīng)很少見這種耳熟能詳?shù)膯栴}了,網(wǎng)絡(luò)上八股文不少。今天,我更想分享一下從源碼的層面來區(qū)別這八竿子打不著的兩者。本篇針對watch做分析,下一篇分析computed。

一、watch參數(shù)類型

我們知道,vue3里的watch接收三個參數(shù):偵聽的數(shù)據(jù)源source、回調(diào)cb、以及可選的optiions。

1. 選項options

我們可以在options里根據(jù)需要設(shè)置**immediate來控制是否立即執(zhí)行一次回調(diào);設(shè)置deep來控制是否進行深度偵聽;設(shè)置flush來控制回調(diào)的觸發(fā)時機,默認為{ flush: 'pre' },即vue組件更新前;若設(shè)置為{ flush: 'post' }則回調(diào)將在vue組件更新之后觸發(fā);此外還可以設(shè)置為{ flush: 'sync' },表示同步觸發(fā);以及設(shè)置收集依賴時的onTrack和觸發(fā)更新時的onTrigger兩個listener,主要用于debugger。watch函數(shù)會返回一個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. 回調(diào)cb

了解完options,接下來我們看看回調(diào)**cb**。通常我們的cb接收三個參數(shù):valueoldValueonCleanUp,然后執(zhí)行我們需要的操作,比如偵聽表格的頁碼,發(fā)生變化時重新請求數(shù)據(jù)。第三個參數(shù)onCleanUp,用于注冊副作用清理的回調(diào)函數(shù), 在副作用下次執(zhí)行之前,這個回調(diào)函數(shù)會被調(diào)用,通常用來清除不需要的或者無效的副作用。

// 副作用
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. 數(shù)據(jù)源source

watch函數(shù)可以偵聽單個數(shù)據(jù)或者多個數(shù)據(jù),共有四種重載,對應(yīng)四種類型的source。其中,單個數(shù)據(jù)源的類型有WatchSource和響應(yīng)式的object,多個數(shù)據(jù)源的類型為MultiWatchSources,Readonly<MultiWatchSources>,而MultiWatchSources其實也就是由單個數(shù)據(jù)源組成的數(shù)組。

// 單數(shù)據(jù)源類型:可以是 Ref 或 ComputedRef 或 函數(shù)
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
?
// 多數(shù)據(jù)源類型
type MultiWatchSources = (WatchSource<unknown> | object)[]
?

二、watch函數(shù)

下面是源碼中的類型聲明,以及watch的重載簽名和實現(xiàn)簽名:

// watch的重載與實現(xiàn)
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的實現(xiàn)簽名中可以看到,和watchEffect不同,watch的第二個參數(shù)cb必須是函數(shù),否則會警告。最后,尾調(diào)用了doWatch,那么具體的實現(xiàn)細節(jié)就都得看doWatch了。讓我們來瞅瞅它到底是何方神圣。

三、watch的核心:doWatch 函數(shù)

先瞄一下doWatch的簽名:接收的參數(shù)大體和watch一致,其中source里多了個WatchEffect類型,這是由于在watchApi.js文件里,還導(dǎo)出了三個函數(shù):watchEffect、watchSyncEffectwatchPostEffect,它們接收的第一個參數(shù)的類型就是WatchEffect,然后傳遞給doWatch,會在后面講到,也可能不會;而options默認值為空對象,函數(shù)返回一個WatchStopHandle,用于停止偵聽。

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

再來看看doWatch的函數(shù)體,了解一下它干了些啥:

首先是判斷在沒有cb的情況下,如果options里設(shè)置了immediatedeep,就會告警,這倆屬性只對有cbdoWatch簽名有效。其實也就是上面說到的watchEffect等三個函數(shù),它們是沒有cb這個參數(shù)的,因此它們設(shè)置的immediatedeep是無效的。聲明一個當(dāng)source參數(shù)不合法時的警告函數(shù),代碼如下:

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參數(shù)不合法的警告函數(shù)
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.`
    )
  }
// ...

接下來,就到了正文了。第一步的目標(biāo)是設(shè)置getter,順便配置一下強制觸發(fā)和深層偵聽等。拿到getter的目的是為了之后創(chuàng)建effect,vue3的響應(yīng)式離不開effect,日后再出一篇文章介紹。

先拿到當(dāng)前實例,聲明了空的getter,初始化關(guān)閉強制觸發(fā),且默認為單數(shù)據(jù)源的偵聽,然后根據(jù)傳入的source的類型,做不同的處理:

  • Ref: getter返回值為Ref的·value,強制觸發(fā)由source是否為淺層的Ref決定;
  • Reactive響應(yīng)式對象:getter的返回值為source本身,且設(shè)置深層偵聽;
  • Arraysource為數(shù)組,則是多數(shù)據(jù)源偵聽,將isMultiSource設(shè)置為true,強制觸發(fā)由數(shù)組中是否存在Reactive響應(yīng)式對象或者淺層的Ref來決定;并且設(shè)置getter的返回值為從source映射而來的新數(shù)組;
  • function:當(dāng)source為函數(shù)時,會判斷有無cb,有cb則是watch,否則是watchEffect等。當(dāng)有cb時,使用callWithErrorHandling包裹一層來調(diào)用source得到的結(jié)果,作為getter的返回值;
  • otherTypes:其它類型,則告警source參數(shù)不合法,且getter設(shè)置為NOOP,一個空的函數(shù)。
// 拿到當(dāng)前實例,聲明了空的getter,初始化關(guān)閉強制觸發(fā),且默認為單數(shù)據(jù)源的偵聽
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
?
// 根據(jù)偵聽數(shù)據(jù)源的類型做相應(yīng)的處理
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 = () =>
      // 可見,數(shù)組成員只能是Ref、Reactive或者函數(shù),其它類型無法通過校驗,將引發(fā)告警
      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,該函數(shù)的作用是對getter的返回值做一個遞歸遍歷,將遍歷到的值添加到一個叫做seen的集合中,seen的成員即為當(dāng)前watch要偵聽的那些數(shù)據(jù)。代碼如下(影響主線可先跳過):

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)) {
    // 數(shù)組
    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就設(shè)置好了。之后聲明了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函數(shù)中取得newValue,這不就是我們在使用watch的時候的熟悉套路嘛。

let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
// job為當(dāng)前watch要做的工作,后續(xù)通過調(diào)度器來處理
const job: SchedulerJob = () => {
  // 當(dāng)前effect不在active狀態(tài),說明沒有觸發(fā)該effect的響應(yīng)式變化,直接返回 
  if (!effect.active) {
    return
  }
  // cb存在,說明是watch,而不是watchEffect
  if (cb) {
    // watch(source, cb)
    // 調(diào)用 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()
      }
      // 用異步異常處理程序包裹了一層來調(diào)用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執(zhí)行完成,當(dāng)前的新值就變成了舊值
      oldValue = newValue
    }
  } else {
    // cb不存在,則是watchEffect
    // watchEffect
    effect.run()
  }
}
// 設(shè)置allowRecurse,讓調(diào)度器知道它可以自己觸發(fā)
job.allowRecurse = !!cb

一看job里,在watch的分支出現(xiàn)了effect,但是這個分支并沒有effect呀,再往下看,噢,原來是由之前取得的getter來創(chuàng)建的effect。在這之前,還定義了調(diào)度器,調(diào)度器scheduler被糅合進了effect里,影響了newValue的獲取,從而影響cb的調(diào)用時機:

  • sync:同步執(zhí)行,也就是回調(diào)cb直接執(zhí)行;
  • pre:默認值是pre,表示組件更新前執(zhí)行;
  • post:組件更新后執(zhí)行。
let scheduler: EffectScheduler
// 根據(jù)flush的值來創(chuàng)建不同的調(diào)度器
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 創(chuàng)建 effect ,watchEffect就不必了,因為自帶的有
const effect = new ReactiveEffect(getter, scheduler)
// 主要是調(diào)試用的onTrack和onTrigger,當(dāng)收集依賴和觸發(fā)更新時做一些操作
if (__DEV__) {
  effect.onTrack = onTrack
  effect.onTrigger = onTrigger
}

現(xiàn)在來到了doWatch最后的環(huán)節(jié)了:偵聽器的初始化。

  • immediate:如果為真值。將直接調(diào)用一次job,上文我們知道,job是包裹了一層錯誤處理程序來調(diào)用cb,所以我們現(xiàn)在終于親眼看到了為什么immediate能讓cb立即觸發(fā)一次。
// initial run
// 有cb,是 watch
if (cb) {
  if (immediate) {
    job()
  } else {
    // 獲取一下當(dāng)前的值作為舊值
    oldValue = effect.run()
  }
} else if (flush === 'post') {
  // 沒有cb,是watchEffect,副作用的時機在組件更新之后,用queuePostRenderEffect包裹一層來調(diào)整時機
  queuePostRenderEffect(
    effect.run.bind(effect),
    instance && instance.suspense
  )
} else {
  // watchEffect,副作用的時機在組件更新之前,直接執(zhí)行一次effect.run
  effect.run()
}
// 返回一個WatchStopHandle,內(nèi)部執(zhí)行 effect.stop來達到停止偵聽的作用
return () => {
  effect.stop()
  // 移除當(dāng)前實例作用域下的當(dāng)前effect
  if (instance && instance.scope) {
    remove(instance.scope.effects!, effect)
  }
}

到這里,watch的源碼算是差不多結(jié)束了。小結(jié)一下核心流程:

  • watch:判斷若沒有cb則告警;
  • watch:尾調(diào)用doWatch,之后的操作都在doWatch里進行;
  • doWatch:判斷沒有cb時若設(shè)置了deepimmediate則告警;
  • doWatch:根據(jù)source的類型得到getter;
  • doWatch:如果cb存在且deep為真則對getter()進行遞歸遍歷;
  • doWatch:獲取oldValue,聲明job函數(shù),在job內(nèi)部獲取newValue并使用callWithAsyncErrorHandling來調(diào)用cb。
  • doWatch:根據(jù)post的值定義的調(diào)度器scheduler;
  • doWatch:根據(jù)getterscheduler創(chuàng)建effect;
  • doWatch:初始化偵聽器,如果有cbimmediate為真值,則立即調(diào)用job函數(shù),相當(dāng)于調(diào)用我們寫的cb;如果immediate為假值,則只調(diào)用effect.run()來初始化oldValue
  • doWatch:返回一個WatchStopHandle,內(nèi)部通過effect.stop()來實現(xiàn)停止偵聽。
  • watch:接收到doWatch返回的WatchStopHandle,并返回給外部使用。

以上就是Vue3源碼解析watch函數(shù)實例的詳細內(nèi)容,更多關(guān)于Vue3 watch函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論