<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
所謂的watch,其本質就是觀測一個響應式資料,當資料發生變化時通知並執行相應的回撥函數。實際上,watch 的實現本質就是利用了 effect 和 options.scheduler 選項。如下例子所示:
// watch 函數接收兩個引數,source 是響應式資料,cb 是回撥函數 function watch(source, cb){ effect( // 觸發讀取操作,從而建立聯絡 () => source.foo, { scheduler(){ // 當資料變化時,呼叫回撥函數 cb cb() } } ) }
如上面的程式碼所示嗎,source 是響應式資料,cb 是回撥函數。如果副作用函數中存在 scheduler 選項,當響應式資料發生變化時,會觸發 scheduler 函數執行,而不是直接觸發副作用函數執行。從這個角度來看, scheduler 排程函數就相當於是一個回撥函數,而 watch 的實現就是利用了這點。
偵聽的資料來源可以 是一個陣列,如下面的函數簽名所示:
// 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
也可以使用陣列同時偵聽多個源,如下面的函數簽名所示:
// 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
偵聽的資料來源是一個 ref 型別的資料 或者是一個具有返回值的 getter 函數,如下面的函數簽名所示:
// 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)
偵聽的資料來源是一個響應式的 obj 物件,如下面的函數簽名所示:
// 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
// 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) }
可以看到,watch 函數接收3個引數,分別是:source 偵聽的資料來源,cb 回撥函數,options 偵聽選項。
從watch的函數過載中可以知道,當偵聽的是單一源時,source 可以是一個 ref 型別的資料 或者是一個具有返回值的 getter 函數,也可以是一個響應式的 obj 物件。當偵聽的是多個源時,source 可以是一個陣列。
在 cb 回撥函數中,給開發者提供了最新的value,舊的value以及onCleanup函數用與清除副作用。如下面的型別定義所示:
export type WatchCallback<V = any, OV = any> = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any
options 選項可以控制 watch 的行為,例如通過options的選項引數immediate來控制watch的回撥是否立即執行,通過options的選項引數來控制watch的回撥函數是同步執行還是非同步執行。options 引數的型別定義如下:
export interface WatchOptionsBase extends DebuggerOptions { flush?: 'pre' | 'post' | 'sync' } export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase { immediate?: Immediate deep?: boolean }
可以看到 options 的型別定義 WatchOptions 繼承了 WatchOptionsBase。也就是說,watch 的 options 中除了 immediate 和 deep 這兩個特有的引數外,還可以傳遞 WatchOptionsBase 中的所有引數以控制副作用執行的行為。
在 watch 的函數體中呼叫了 doWatch 函數,我們來看看它的實現。
實際上,無論是watch函數,還是 watchEffect 函數,在執行時最終呼叫的都是 doWatch 函數。
function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle
doWatch 的函數簽名與 watch 的函數簽名基本一致,也是接收三個引數。在 doWatch 函數中,為了便於options 選項的使用,對 options 進行了解構。
首先從 component 中獲取當前的元件範例,然後分別定義三個變數。其中 getter 是一個函數,她或作為副作用的函數引數傳入到副作用函數中。forceTrigger 變數是一個布林值,用來標識是否需要強制觸發副作用函數執行。isMultiSource 變數同樣也是一個布林值,用來標記偵聽的資料來源是單一源還是以陣列形式傳入的多個源,初始值為 false,表示偵聽的是單一源。如下面的程式碼所示:
const instance = currentInstance let getter: () => any // 是否需要強制觸發副作用函數執行 let forceTrigger = false // 偵聽的是否是多個源 let isMultiSource = false
接下來根據偵聽的資料來源來初始化這三個變數。
偵聽的資料來源是一個 ref 型別的資料
當偵聽的資料來源是一個 ref 型別的資料時,通過返回 source.value 來初始化 getter,也就是說,當 getter 函數被觸發時,會通過source.value 獲取到實際偵聽的資料。然後通過 isShallow 函數來判斷偵聽的資料來源是否是淺響應,並將其結果賦值給 forceTrigger,完成 forceTrigger 變數的初始化。如下面的程式碼所示:
if (isRef(source)) { // 偵聽的資料來源是 ref getter = () => source.value // 判斷資料來源是否是淺響應 forceTrigger = isShallow(source) }
偵聽的資料來源是一個響應式資料
當偵聽的資料來源是一個響應式資料時,直接返回 source 來初始化 getter ,即 getter 函數被觸發時直接返回 偵聽的資料來源。由於響應式資料中可能會是一個object 物件,因此將 deep 設定為 true,在觸發 getter 函數時可以遞迴地讀取物件的屬性值。如下面的程式碼所示:
else if (isReactive(source)) { // 偵聽的資料來源是響應式資料 getter = () => source deep = true }
偵聽的資料來源是一個陣列
當偵聽的資料來源是一個陣列,即同時偵聽多個源。此時直接將 isMultiSource 變數設定為 true,表示偵聽的是多個源。接著通過陣列的 some 方法來檢測偵聽的多個源中是否存在響應式物件,將其結果賦值給 forceTrigger 。然後遍歷陣列,判斷每個源的型別,從而完成 getter 函數的初始化。如下面的程式碼所示:
else if (isArray(source)) { // 偵聽的資料來源是一個陣列,即同時偵聽多個源 isMultiSource = true forceTrigger = source.some(isReactive) getter = () => // 遍歷陣列,判斷每個源的型別 source.map(s => { if (isRef(s)) { // 偵聽的資料來源是 ref return s.value } else if (isReactive(s)) { // 偵聽的資料來源是響應式資料 return traverse(s) } else if (isFunction(s)) { // 偵聽的資料來源是一個具有返回值的 getter 函數 return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) }
偵聽的資料來源是一個函數
當偵聽的資料來源是一個具有返回值的 getter 函數時,判斷 doWatch 函數的第二個引數 cb 是否有傳入。如果有傳入,則處理的是 watch 函數的場景,此時執行 source 函數,將執行結果賦值給 getter 。如果沒有傳入,則處理的是 watchEffect 函數的場景。在該場景下,如果元件範例已經解除安裝,則直接返回,不執行 source 函數。否則就執行 cleanup 清除依賴,然後執行 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] ) } } }
如果偵聽的資料來源是一個響應式資料,需要遞迴讀取響應式資料中的屬性值。如下面的程式碼所示:
// 處理的是 watch 的場景 // 遞迴讀取物件的屬性值 if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }
在上面的程式碼中,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) } }
為了便於控制 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's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup ]) // 更新舊值,不然下一次會得到錯誤的舊值 oldValue = newValue } } else { // watchEffect // 處理 watchEffect 的場景 effect.run() } }
在 job 函數中,判斷回撥函數 cb 是否傳入,如果有傳入,那麼是 watch 函數被呼叫的場景,否則就是 watchEffect 函數被呼叫的場景。
如果是 watch 函數被呼叫的場景,首先執行副作用函數,將執行結果賦值給 newValue 變數,作為最新的值。然後判斷需要執行回撥函數 cb 的情況:
只要滿足上面三種情況中的其中一種,就需要執行 watch 函數的回撥函數 cb。如果回撥函數 cb 是再次執行,在執行之前需要先清除副作用。然後呼叫 callWithAsyncErrorHandling 函數執行回撥函數cb,並將新值newValue 和舊值 oldValue 傳入回撥函數cb中。在回撥函數cb執行後,更新舊值oldValue,避免在下一次執行回撥函數cb時獲取到錯誤的舊值。
如果是 watchEffect 函數被呼叫的場景,則直接執行副作用函數即可。
根據是否傳入回撥函數cb,設定 job 函數的 allowRecurse 屬性。這個設定十分重要,它能夠讓 job 作為偵聽器的回撥,這樣排程器就能知道它允許呼叫自身。
// important: mark the job as a watcher callback so that scheduler knows // it is allowed to self-trigger (#1727) // 重要:讓排程器任務作為偵聽器的回撥以至於排程器能知道它可以被允許自己派發更新 job.allowRecurse = !!cb
在呼叫 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)
在執行副作用函數之前,首先判斷是否傳入了回撥函數cb,如果有傳入,則根據 options 的 immediate 選項來判斷是否需要立即執行回撥函數cb,如果指定了immediate 選項,則立即執行 job 函數,即 watch 的回撥函數會在 watch 建立時立即執行一次。否則就手動呼叫副作用函數,並將返回值作為舊值,賦值給 oldValue。如下面的程式碼所示:
if (cb) { // 選項引數 immediate 來指定回撥是否需要立即執行 if (immediate) { // 回撥函數會在 watch 建立時立即執行一次 job() } else { // 手動呼叫副作用函數,拿到的就是舊值 oldValue = effect.run() } }
如果 options 的 flush 選項的值為 post ,需要將副作用函數放入到微任務佇列中,等待元件掛載完成後再執行副作用函數。如下面的程式碼所示:
else if (flush === 'post') { // 在排程器函數中判斷 flush 是否為 'post',如果是,將其放到微任務佇列中執行 queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ) }
其餘情況都是立即執行副作用函數。如下面的程式碼所示:
else { // 其餘情況立即首次執行副作用 effect.run() }
doWatch 函數最後返回了一個匿名函數,該函數用以結束資料來源的偵聽。因此在呼叫 watch 或者 watchEffect 時,可以呼叫其返回值類結束偵聽。
return () => { effect.stop() if (instance && instance.scope) { // 返回一個函數,用以顯式的結束偵聽 remove(instance.scope.effects!, effect) } }
watch 的本質就是觀測一個響應式資料,當資料發生變化時通知並執行相應的回撥函數。watch的實現利用了effect 和 options.scheduler 選項。
watch 可以偵聽單一源,也可以偵聽多個源。偵聽單一源時資料來源可以是一個具有返回值的getter 函數,或者是一個 ref 物件,也可以是一個響應式的 object 物件。偵聽多個源時,其資料來源是一個陣列。
在watch的實現中,根據偵聽的資料來源的型別來初始化getter 函數和 scheduler 排程函數,根據這兩個函數建立一個副作用函數,並根據 options 的 immediate 選項以及 flush 選項來指定回撥函數和副作用函數的執行時機。當 immediate 為 true 時,在watch 建立時會立即執行一次回撥函數。當 flush 的值為 post 時,scheduler 排程函數和副作用函數都會被新增到微任務佇列中,會等待 DOM 更新結束後再執行。
到此這篇關於Vue3原始碼分析偵聽器watch的實現原理的文章就介紹到這了,更多相關Vue3偵聽器watch內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45