Vue3源碼解析watch函數(shù)實(shí)例
引言
想起上次面試,問了個(gè)古老的問題:watch和computed的區(qū)別。多少有點(diǎn)感慨,現(xiàn)在已經(jīng)很少見這種耳熟能詳?shù)膯栴}了,網(wǎng)絡(luò)上八股文不少。今天,我更想分享一下從源碼的層面來區(qū)別這八竿子打不著的兩者。本篇針對(duì)watch做分析,下一篇分析computed。
一、watch參數(shù)類型
我們知道,vue3里的watch接收三個(gè)參數(shù):偵聽的數(shù)據(jù)源source、回調(diào)cb、以及可選的optiions。
1. 選項(xiàng)options
我們可以在options里根據(jù)需要設(shè)置**immediate來控制是否立即執(zhí)行一次回調(diào);設(shè)置deep來控制是否進(jìn)行深度偵聽;設(shè)置flush來控制回調(diào)的觸發(fā)時(shí)機(jī),默認(rèn)為{ flush: 'pre' },即vue組件更新前;若設(shè)置為{ flush: 'post' }則回調(diào)將在vue組件更新之后觸發(fā);此外還可以設(shè)置為{ flush: 'sync' },表示同步觸發(fā);以及設(shè)置收集依賴時(shí)的onTrack和觸發(fā)更新時(shí)的onTrigger兩個(gè)listener,主要用于debugger。watch函數(shù)會(huì)返回一個(gè)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接收三個(gè)參數(shù):value、oldValue和onCleanUp,然后執(zhí)行我們需要的操作,比如偵聽表格的頁碼,發(fā)生變化時(shí)重新請(qǐng)求數(shù)據(jù)。第三個(gè)參數(shù)onCleanUp,用于注冊(cè)副作用清理的回調(diào)函數(shù), 在副作用下次執(zhí)行之前,這個(gè)回調(diào)函數(shù)會(huì)被調(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ù)可以偵聽單個(gè)數(shù)據(jù)或者多個(gè)數(shù)據(jù),共有四種重載,對(duì)應(yīng)四種類型的source。其中,單個(gè)數(shù)據(jù)源的類型有WatchSource和響應(yīng)式的object,多個(gè)數(shù)據(jù)源的類型為MultiWatchSources,Readonly<MultiWatchSources>,而MultiWatchSources其實(shí)也就是由單個(gè)數(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的重載簽名和實(shí)現(xiàn)簽名:
// watch的重載與實(shí)現(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的實(shí)現(xiàn)簽名中可以看到,和watchEffect不同,watch的第二個(gè)參數(shù)cb必須是函數(shù),否則會(huì)警告。最后,尾調(diào)用了doWatch,那么具體的實(shí)現(xiàn)細(xì)節(jié)就都得看doWatch了。讓我們來瞅瞅它到底是何方神圣。
三、watch的核心:doWatch 函數(shù)
先瞄一下doWatch的簽名:接收的參數(shù)大體和watch一致,其中source里多了個(gè)WatchEffect類型,這是由于在watchApi.js文件里,還導(dǎo)出了三個(gè)函數(shù):watchEffect、watchSyncEffect和watchPostEffect,它們接收的第一個(gè)參數(shù)的類型就是WatchEffect,然后傳遞給doWatch,會(huì)在后面講到,也可能不會(huì);而options默認(rèn)值為空對(duì)象,函數(shù)返回一個(gè)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,就會(huì)告警,這倆屬性只對(duì)有cb的doWatch簽名有效。其實(shí)也就是上面說到的watchEffect等三個(gè)函數(shù),它們是沒有cb這個(gè)參數(shù)的,因此它們?cè)O(shè)置的immediate和deep是無效的。聲明一個(gè)當(dāng)source參數(shù)不合法時(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.`
)
}
}
?
// 聲明一個(gè)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,順便配置一下強(qiáng)制觸發(fā)和深層偵聽等。拿到getter的目的是為了之后創(chuàng)建effect,vue3的響應(yīng)式離不開effect,日后再出一篇文章介紹。
先拿到當(dāng)前實(shí)例,聲明了空的getter,初始化關(guān)閉強(qiáng)制觸發(fā),且默認(rèn)為單數(shù)據(jù)源的偵聽,然后根據(jù)傳入的source的類型,做不同的處理:
Ref:getter返回值為Ref的·value,強(qiáng)制觸發(fā)由source是否為淺層的Ref決定;Reactive響應(yīng)式對(duì)象:getter的返回值為source本身,且設(shè)置深層偵聽;Array:source為數(shù)組,則是多數(shù)據(jù)源偵聽,將isMultiSource設(shè)置為true,強(qiáng)制觸發(fā)由數(shù)組中是否存在Reactive響應(yīng)式對(duì)象或者淺層的Ref來決定;并且設(shè)置getter的返回值為從source映射而來的新數(shù)組;function:當(dāng)source為函數(shù)時(shí),會(huì)判斷有無cb,有cb則是watch,否則是watchEffect等。當(dāng)有cb時(shí),使用callWithErrorHandling包裹一層來調(diào)用source得到的結(jié)果,作為getter的返回值;otherTypes:其它類型,則告警source參數(shù)不合法,且getter設(shè)置為NOOP,一個(gè)空的函數(shù)。
// 拿到當(dāng)前實(shí)例,聲明了空的getter,初始化關(guān)閉強(qiáng)制觸發(fā),且默認(rèn)為單數(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ù),其它類型無法通過校驗(yàn),將引發(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等),對(duì)getter做個(gè)traverse,該函數(shù)的作用是對(duì)getter的返回值做一個(gè)遞歸遍歷,將遍歷到的值添加到一個(gè)叫做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)) {
// 普通對(duì)象
for (const key in value) {
traverse((value as any)[key], seen)
}
}
return value
}
至此,getter就設(shè)置好了。之后聲明了cleanup和onCleanup,用于清除副作用。以及SSR檢測(cè)。雖然不是本文的重點(diǎn),但還是貼一下源碼:
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,這不就是我們?cè)谑褂?code>watch的時(shí)候的熟悉套路嘛。
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 ||
// 取到的新值和舊值是否相同,如果有變化則進(jìn)入分支
(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,但是這個(gè)分支并沒有effect呀,再往下看,噢,原來是由之前取得的getter來創(chuàng)建的effect。在這之前,還定義了調(diào)度器,調(diào)度器scheduler被糅合進(jìn)了effect里,影響了newValue的獲取,從而影響cb的調(diào)用時(shí)機(jī):
sync:同步執(zhí)行,也就是回調(diào)cb直接執(zhí)行;pre:默認(rèn)值是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就不必了,因?yàn)樽詭У挠?
const effect = new ReactiveEffect(getter, scheduler)
// 主要是調(diào)試用的onTrack和onTrigger,當(dāng)收集依賴和觸發(fā)更新時(shí)做一些操作
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
現(xiàn)在來到了doWatch最后的環(huán)節(jié)了:偵聽器的初始化。
immediate:如果為真值。將直接調(diào)用一次job,上文我們知道,job是包裹了一層錯(cuò)誤處理程序來調(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,副作用的時(shí)機(jī)在組件更新之后,用queuePostRenderEffect包裹一層來調(diào)整時(shí)機(jī)
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
// watchEffect,副作用的時(shí)機(jī)在組件更新之前,直接執(zhí)行一次effect.run
effect.run()
}
// 返回一個(gè)WatchStopHandle,內(nèi)部執(zhí)行 effect.stop來達(dá)到停止偵聽的作用
return () => {
effect.stop()
// 移除當(dāng)前實(shí)例作用域下的當(dāng)前effect
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
到這里,watch的源碼算是差不多結(jié)束了。小結(jié)一下核心流程:
watch:判斷若沒有cb則告警;watch:尾調(diào)用doWatch,之后的操作都在doWatch里進(jìn)行;doWatch:判斷沒有cb時(shí)若設(shè)置了deep或immediate則告警;doWatch:根據(jù)source的類型得到getter;doWatch:如果cb存在且deep為真則對(duì)getter()進(jìn)行遞歸遍歷;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:返回一個(gè)WatchStopHandle,內(nèi)部通過effect.stop()來實(shí)現(xiàn)停止偵聽。watch:接收到doWatch返回的WatchStopHandle,并返回給外部使用。
以上就是Vue3源碼解析watch函數(shù)實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Vue3 watch函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue+echarts繪制省份地圖并添加自定義標(biāo)注方式
這篇文章主要介紹了vue+echarts繪制省份地圖并添加自定義標(biāo)注方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
ElementUI中的el-dropdown傳入多參數(shù)的實(shí)現(xiàn)方法
本文主要介紹了ElementUI中的el-dropdown傳入多參數(shù)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12
基于Vue開發(fā)一個(gè)很火的卡片動(dòng)畫效果
這篇文章主要為大家詳細(xì)介紹了如何基于Vue開發(fā)一個(gè)很火的卡片動(dòng)畫效果,大致包含兩個(gè)效果,光的跟隨效果還有卡片傾斜像?3D?的效果,感興趣的可以了解一下2024-02-02
關(guān)于el-col的使用,占據(jù)寬度的應(yīng)用解析
這篇文章主要介紹了關(guān)于el-col的使用,占據(jù)寬度的應(yīng)用解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
Vue項(xiàng)目開發(fā)實(shí)現(xiàn)父組件與子組件數(shù)據(jù)間的雙向綁定原理及適用場(chǎng)景
在 Vue.js 中,實(shí)現(xiàn)父組件與子組件數(shù)據(jù)之間的雙向綁定,可以通過以下幾種方式,下面我將介紹幾種常見的方法,并解釋它們的實(shí)現(xiàn)原理和適用場(chǎng)景,感興趣的朋友跟隨小編一起看看吧2024-12-12
vue中想要mock數(shù)據(jù)在線上環(huán)境生效如何操作
本文主要介紹了在配置了mock數(shù)據(jù)之后在線上環(huán)境使用,主要通過在main.ts文件中注冊(cè)和vite.config.ts文件夾中配置插件來實(shí)現(xiàn),感興趣的可以了解一下2025-01-01

