本篇文章筆者會講解 Vue3 中偵聽器相關的 api:watchEffect 和 watch 。在 Vue3 以前 watch 是 option 寫法中一個很經常使用的選項,使用它能夠很是方便的監聽一個數據源的變化,而在 Vue3 中隨着 Composition API 的寫法推行也將 watch 獨立成了一個 響應式 api,今天咱們就一塊兒來學習 watch 相關的偵聽器是如何實現的。vue
👇 儲備知識要求:react
在閱讀本文前,建議你已經學習過本系列的第 7 篇文章的 effect 反作用函數的相關知識,不然在講解反作用的相關部分可能會出現不理解的狀況。git
因爲 watch api 中的許多行爲都與 watchEffect api 一致,因此筆者將 watchEffect 放在首位講解,爲了根據響應式狀態自動應用和從新應用反作用,咱們可使用 watchEffect 方法。它當即執行傳入的一個函數,同時響應式追蹤其依賴,並在以來變動時從新運行該函數。github
watchEffect 函數的實現很是簡潔:api
export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null, options) }
首先來看參數類型:數組
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void export interface WatchOptionsBase { flush?: 'pre' | 'post' | 'sync' onTrack?: ReactiveEffectOptions['onTrack'] onTrigger?: ReactiveEffectOptions['onTrigger'] } export type WatchStopHandle = () => void
第一個參數 effect,接收函數類型的變量,而且在這個函數中會傳入 onInvalidate 參數,用以清除反作用。閉包
第二個參數 options 是一個對象,在這個對象中有三個屬性,你能夠修改 flush 來改變反作用的刷新時機,默認爲 pre,當修改成 post 時,就能夠在組件更新後觸發這個反作用偵聽器,改同 sync 會強制同步觸發。而 onTrack 和 onTrigger 選項能夠用於調試偵聽器的行爲,而且兩個參數只能在開發模式下工做。併發
參數傳入後,函數會執行並返回 doWatch 函數的返回值。異步
因爲 watch api 也會調用 doWatch 函數,因此 doWatch 函數的具體邏輯咱們會放在後邊講。先看 watch api 的函數實現。函數
這個獨立出來的 watch api 與組件中的 watch option 是徹底等同的,watch 須要偵聽特定的數據源,並在回調函數中執行反作用。默認狀況下這個偵聽是惰性的,即只有當被偵聽的源發生變化時才執行回調。
與 watchEffect 相比,watch 有如下不一樣:
watch 函數的函數簽名有許多種重載狀況,且代碼行數較多,因此筆者不許備分析每一個重載狀況,一塊兒來看一下 watch api 的實現。
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 的類型以下:
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) type MultiWatchSources = (WatchSource<unknown> | object)[]
從兩個類型定義看出,數據源支持傳入單個的 Ref、Computed 響應式對象,或者傳入一個返回相同泛型類型的函數,以及 source 支持傳入數組,以便能同時監聽多個數據源。
在這個最通用的聲明中,cb 的類型是 any,可是其實 cb 這個回調函數也有他本身的類型:
export type WatchCallback<V = any, OV = any> = ( value: V, oldValue: OV, onInvalidate: InvalidateCbRegistrator ) => any
在回調函數中,會提供最新的 value、舊 value,以及 onInvalidate 函數用以清除反作用。
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase { immediate?: Immediate deep?: boolean }
能夠看到 options 的類型 WatchOptions 繼承了 WatchOptionsBase,這也就是 watch 除了 immediate 和 deep 這兩個特有的參數外,還能夠傳遞 WatchOptionsBase 中的全部參數以控制反作用執行的行爲。
分析完參數後,能夠看到函數體內的邏輯與 watchEffect 幾乎一致,可是多了在開發環境下檢測回調函數是不是函數類型,若是回調函數不是函數,就會報警。
執行 doWatch 時的傳參與 watchEffect 相比,多了第二個參數回調函數。
下面就讓咱們揭開這個終極 boss doWatch 的廬山真面目吧。
不論是 watchEffect、watch 仍是組件內的 watch 選項,在執行時最終調用的都是 doWatch 中的邏輯,這個強大的 doWatch 函數爲了兼容各個 api 的邏輯源碼也是挺長的大約有 200 行,因此老規矩,筆者會將長源碼拆分開來說。若想閱讀完整源碼請戳這裏。
先從 doWatch 的函數簽名看起:
function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle
這個函數簽名與 watch 基本一致,多了一個 instance 的參數,默認值爲 currentInstance,currentInstance 是當前調用組件暴露出來的一個變量,方便該偵聽器找到本身對應的組件。
而 source 在這裏的類型就比較清晰,支持單個的 source 或者數組,也只是一個普通對象。
接着會建立三個變量,getter 最終會當作反作用的函數參數傳入,forceTrigger 標識是否須要強制更新,isMultiSource 標記傳入的是單個數據源仍是以數組形式傳入的多個數據源。
let getter: () => any let forceTrigger = false let isMultiSource = false
而後會開始判斷 source 的類型,根據不一樣的類型重置這三個參數的值。
ref 類型
reactive 類型
數組 array 類型
source 是函數 function 類型
若是有回調函數
若是沒有回調函數,那麼此時就是 watchEffect api 的場景了。
此時會爲 watchEffect 設置 getter 函數,getter 函數邏輯以下:
相關代碼以下,因爲邏輯已經完整的一絲不落的在上面分析了,因此就容筆者偷個懶,不加註釋了。
if (isRef(source)) { // ref 類型的數據源,更新 getter 與 forceTrigger getter = () => (source as Ref).value forceTrigger = !!(source as Ref)._shallow } else if (isReactive(source)) { // reactive 類型的數據源,更新 getter 與 deep getter = () => source deep = true } else if (isArray(source)) { // 多個數據源,更新 isMultiSource、forceTrigger、getter isMultiSource = true forceTrigger = source.some(isReactive) // 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) } }) } else if (isFunction(source)) { // 數據源是函數的狀況 if (cb) { // 若是有回調,則更新 getter,讓數據源做爲 getter 函數 getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // 沒有回調即爲 watchEffect 場景 getter = () => { if (instance && instance.isUnmounted) { return } if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onInvalidate] ) } } } else { // 其他狀況 getter 爲空函數,併發出警告 getter = NOOP __DEV__ && warnInvalidSource(source) }
接着會處理 watch 中的場景,當有回調,而且 deep 選項爲 true 時,將使用 traverse 來包裹 getter 函數,對數據源中的每一個屬性遞歸遍歷進行監聽。
if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }
以後會聲明 cleanup 和 onInvalidate 函數,並在 onInvalidate 函數的執行過程當中給 cleanup 函數賦值,當反作用函數執行一些異步的反作用,這些響應須要在其失效時清除,因此偵聽反作用傳入的函數能夠接收一個 onInvalidate 函數做爲入參,用來註冊清理失效時的回調。當如下狀況發生時,這個失效回調會被觸發:
let cleanup: () => void let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } }
接着會初始化 oldValue 並賦值。
而後聲明一個 job 函數,這個函數最終會做爲調度器中的回調函數傳入,因爲是一個閉包形式依賴外部做用域中的許多變量,因此會放在後面講,避免出現還未聲明的變量形成理解困難。
根據是否有回調函數,設置 job 的 allowRecurse 屬性,這個設置很重要,可以讓 job 做爲一個觀察者的回調這樣調度器就能知道它容許調用自身。
接着聲明一個 scheduler 的調度器對象,根據 flush 的傳參來肯定調度器的執行時機。
這一部分邏輯的源碼以下:
// 初始化 oldValue let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE const job: SchedulerJob = () => { /*暫時忽略邏輯*/ } // 聲明一個 job 調度器任務,暫時不關注內部邏輯 // 重要:讓調度器任務做爲偵聽器的回調以致於調度器能知道它能夠被容許本身派發更新 job.allowRecurse = !!cb 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() } } }
在處理完以上的調度器部分後,會開始建立反作用。
首先聲明一個 runner 變量,它建立一個反作用並將以前處理好的 getter 函數做爲反作用函數傳入,並在反作用選項中設置了延遲調用,以及設置了對應的調度器。
並經過 recordInstanceBoundEffect 函數將該反作用函數加入組件實例的的 effects 屬性中,好讓組件在卸載時可以主動得中止這些反作用函數的執行。
接着會開始處理首次執行反作用函數。
若是 watch 有回調函數
最後 doWatch 函數會返回一個函數,這個函數的做用是中止偵聽,因此你們在使用時能夠顯式的爲 watch、watchEffect 調用返回值以中止偵聽。
// 建立 runner 反作用 const runner = effect(getter, { lazy: true, onTrack, onTrigger, scheduler }) // 將 runner 添加進 instance.effects 數組中 recordInstanceBoundEffect(runner, instance) // 初始化調用反作用 if (cb) { if (immediate) { job() // 有回調函數且是 imeediate 選項的當即執行調度器任務 } else { oldValue = runner() // 不然執行一次 runner,並將返回值賦值給 oldValue } } else if (flush === 'post') { // 若是調用時機爲 post,則推入延遲執行隊列 queuePostRenderEffect(runner, instance && instance.suspense) } else { // 其他狀況當即首次執行反作用 runner() } // 返回一個函數,用以顯式的結束偵聽 return () => { stop(runner) if (instance) { remove(instance.effects!, runner) } }
doWatch 函數到這裏就所有運行完畢了,如今全部的變量已經聲明完畢,尤爲是最後聲明的 runner 反作用。咱們能夠回過頭看看被調用了屢次的 job 中究竟作了什麼。
調度器任務中作的事情邏輯比較清晰,首先會判斷 runner 反作用是否被停用,若是已經被停用則當即返回,再也不執行後續邏輯。
以後區分場景,經過是否存在回調函數判斷是 watch api 調用仍是 watchEffect api 調用。
若是是 watch api 調用,則會執行 runner 反作用,將其返回值賦值給 newValue,做爲最新的值。若是是 deep 須要深度偵聽,或者是 forceTrigger 須要強制更新,或者新舊值發生了改變,這三種狀況都須要觸發 cb 回調,通知偵聽器發生了變化。在調用偵聽器以前會先經過 cleanup 清除反作用,接着觸發 cb 回調,將 newValue、oldValue、onInvalidate 三個參數傳入回調。在回調觸發後再去更新 oldValue 的值。
而若是沒有 cb 回調函數,即爲 watchEffect 的場景,此時調度器任務僅僅須要執行 runner 反作用函數就好。
job 調度器任務中的具體代碼邏輯以下:
const job: SchedulerJob = () => { if (!runner.active) { // 若是反作用以停用則直接返回 return } if (cb) { // watch(source, cb) 場景 // 調用 runner 反作用獲取最新的值 newValue const newValue = runner() // 若是是 deep 或 forceTrigger 或有值更新 if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) ) { // 當回調再次執行前先清除反作用 if (cleanup) { cleanup() } // 觸發 watch api 的回調,並將 newValue、oldValue、onInvalidate 傳入 callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // 首次調用時,將 oldValue 的值設置爲 undefined oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) oldValue = newValue // 觸發回調後,更新 oldValue } } else { // watchEffect 的場景,直接執行 runner runner() } }
在本文中,筆者給你們詳細講解了 Vue3 中提供的 watch、watchEffect 兩個 api 的實現,而且在組件的 option 選項中的 watch,其實也是經過 doWatch 函數來完成偵聽的。在講解的過程當中,咱們發現 Vue3 中的偵聽器也是經過反作用來實現的,因此理解偵聽器以前須要先了解透徹反作用究竟作了什麼。
咱們看到 watch、watchEffect 的背後都是調用並返回 doWatch 函數,筆者拆解分析了 doWatch 函數,讓讀者可以清楚的知道 doWatch 每一行代碼都作了什麼,以便於當咱們的偵聽器不如本身預期的工做時,能夠從細節之處分析緣由,而不至於瞎猜瞎試。
最後,若是這篇文章可以幫助到你瞭解更瞭解 Vue3 中的 watch 的原理以及它的工做方式,但願能給本文點一個喜歡❤️。若是想繼續追蹤後續文章,也能夠關注個人帳號或 follow 個人 github,再次謝謝各位可愛的看官老爺。