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ù):value
、oldValue
和onCleanUp
,然后執(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
、watchSyncEffect
和watchPostEffect
,它們接收的第一個參數(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è)置了immediate
和deep
,就會告警,這倆屬性只對有cb
的doWatch
簽名有效。其實也就是上面說到的watchEffect
等三個函數(shù),它們是沒有cb
這個參數(shù)的,因此它們設(shè)置的immediate
和deep
是無效的。聲明一個當(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è)置深層偵聽;Array
:source
為數(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 } }
然后判斷了下deep
和cb
,在深度偵聽且有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è)置好了。之后聲明了cleanup
和onCleanup
,用于清除副作用。以及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è)置了deep
或immediate
則告警;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ù)getter
和scheduler
創(chuàng)建effect
;doWatch
:初始化偵聽器,如果有cb
且immediate
為真值,則立即調(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)文章
Vue2.0實現(xiàn)調(diào)用攝像頭進行拍照功能 exif.js實現(xiàn)圖片上傳功能
這篇文章主要為大家詳細介紹了Vue2.0實現(xiàn)調(diào)用攝像頭進行拍照功能,以及圖片上傳功能引用exif.js,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-04-04基于Vue+ELement搭建動態(tài)樹與數(shù)據(jù)表格實現(xiàn)分頁模糊查詢實戰(zhàn)全過程
這篇文章主要給大家介紹了關(guān)于如何基于Vue+ELement搭建動態(tài)樹與數(shù)據(jù)表格實現(xiàn)分頁模糊查詢的相關(guān)資料,Vue Element UI提供了el-pagination組件來實現(xiàn)表格數(shù)據(jù)的分頁功能,需要的朋友可以參考下2023-10-10如何在ElementUI的上傳組件el-upload中設(shè)置header
這篇文章主要介紹了如何在ElementUI的上傳組件el-upload中設(shè)置header,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09淺談vue在html中出現(xiàn){{}}的原因及解決方式
這篇文章主要介紹了淺談vue在html中出現(xiàn){{}}的原因及解決方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11