<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在平時的開發工作中,我們經常使用偵聽器幫助我們去觀察某個資料的變化然後去執行一段邏輯。
在 Vue.js 2.x 中,你可以通過 watch 選項去初始化一個偵聽器,稱作 watcher:
export default { watch: { a(newVal, oldVal) { console.log('new: %s,00 old: %s', newVal, oldVal) } } }
當然你也可以通過 $watch API 去建立一個偵聽器:
const unwatch = vm.$watch('a', function(newVal, oldVal) { console.log('new: %s, old: %s', newVal, oldVal) })
與 watch 選項不同,通過 $watch API 建立的偵聽器 watcher 會返回一個 unwatch 函數,你可以隨時執行它來停止這個 watcher 對資料的偵聽,而對於 watch 選項建立的偵聽器,它會隨著元件的銷燬而停止對資料的偵聽。
在 Vue.js 3.0 中,雖然你仍可以使用 watch 選項,但針對 Composition API,Vue.js 3.0 提供了 watch API 來實現偵聽器的效果。本文就來分析下 watch API 的實現原理
我們先來看 Vue.js 3.0 中 watch API 有哪些用法。
1.watch API 可以偵聽一個 getter 函數,但是它必須返回一個響應式物件,當該響應式物件更新後,會執行對應的回撥函數。
import { reactive, watch } from 'vue' const state = reactive({ count: 0 }) watch(() => state.count, (count, prevCount) => { // 當 state.count 更新,會觸發此回撥函數 })
2.watch API 也可以直接偵聽一個響應式物件,當響應式物件更新後,會執行對應的回撥函數。
import { ref, watch } from 'vue' const count = ref(0) watch(count, (count, prevCount) => { // 當 count.value 更新,會觸發此回撥函數 })
3.watch API 還可以直接偵聽多個響應式物件,任意一個響應式物件更新後,就會執行對應的回撥函數。
import { ref, watch } from 'vue' const count = ref(0) const count2 = ref(1) watch([count, count2], ([count, count2], [prevCount, prevCount2]) => { // 當 count.value 或者 count2.value 更新,會觸發此回撥函數 })
偵聽器的言下之意就是,當偵聽的物件或者函數發生了變化則自動執行某個回撥函數,這和副作用函數 effect 很像, 那它的內部實現是不是依賴了 effect 呢?帶著這個疑問,我們來探究 watch API 的具體實現:
function watch(source, cb, options) { if ((process.env.NODE_ENV !== 'production') && !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, cb, options) } function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { // 標準化 source // 構造 applyCb 回撥函數 // 建立 scheduler 時序執行函數 // 建立 effect 副作用函數 // 返回偵聽器銷燬函數 }
從程式碼中可以看到,watch 函數內部呼叫了 doWatch 函數,呼叫前會在非生產環境下判斷第二個引數 cb 是不是一個函數,如果不是則會報警告以告訴使用者應該使用 watchEffect(fn, options) API,watchEffect API 也是偵聽器相關的 API,稍後我們會詳細介紹。
下面我們就看看doWatch函數做了哪些事情
我們先來看watch 函數的第一個引數 source。
通過前文知道 source 可以是 getter 函數,也可以是響應式物件甚至是響應式物件陣列,所以我們需要標準化 source,這是標準化 source 的流程:
// source 不合法的時候會報警告 const warnInvalidSource = (s) => { 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.`) } // 當前元件範例 const instance = currentInstance let getter if (isArray(source)) { 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, 2 /* WATCH_GETTER */) } else { (process.env.NODE_ENV !== 'production') && warnInvalidSource(s) } }) } else if (isRef(source)) { getter = () => source.value } else if (isReactive(source)) { getter = () => source deep = true } else if (isFunction(source)) { if (cb) { // getter with cb getter = () => callWithErrorHandling(source, instance, 2 /* WATCH_GETTER */) } else { // watchEffect 的邏輯 } } else { getter = NOOP (process.env.NODE_ENV !== 'production') && warnInvalidSource(source) } if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }
其實,source 標準化主要是根據 source 的型別,將其變成 getter 函數。具體來說:
如果 source 不滿足上述條件,則在非生產環境下報警告,提示 source 型別不合法。
我們來看一下最終標準化生成的 getter 函數,它會返回一個響應式物件,在後續建立 effect runner 副作用函數需要用到,每次執行 runner 就會把 getter 函數返回的響應式物件作為 watcher 求值的結果,effect runner 的建立流程我們後續會詳細分析,這裡不需要深入瞭解。
最後我們來關注一下 deep 為 true 的情況。此時,我們會發現生成的 getter 函數會被 traverse 函數包裝一層。traverse 函數的實現很簡單,即通過遞迴的方式存取 value 的每一個子屬性。那麼,為什麼要遞迴存取每一個子屬性呢?
其實 deep 屬於 watcher 的一個設定選項,Vue.js 2.x 也支援,表面含義是深度偵聽,實際上是通過遍歷物件的每一個子屬性來實現。舉個例子你就明白了:
import { reactive, watch } from 'vue' const state = reactive({ count: { a: { b: 1 } } }) watch(state.count, (count, prevCount) => { console.log(count) }) state.count.a.b = 2
這裡,我們利用 reactive API 建立了一個巢狀層級較深的響應式物件 state,然後再呼叫 watch API 偵聽 state.count 的變化。接下來我們修改內部屬性 state.count.a.b 的值,你會發現 watcher 的回撥函數執行了,為什麼會執行呢?
原則上Proxy實現的響應式物件,只有物件屬性先被存取觸發了依賴收集,再去修改這個屬性,才可以通知對應的依賴更新。而從上述業務程式碼來看,我們修改 state.count.a.b 的值時並沒有存取它 ,但還是觸發了 watcher 的回撥函數。
根本原因是,當我們執行 watch 函數的時候,我們知道如果偵聽的是一個 reactive 物件,那麼內部會設定 deep 為 true, 然後執行 traverse 去遞迴存取物件深層子屬性,這個時候就會存取 state.count.a.b 觸發依賴收集,這裡收集的依賴是 watcher 內部建立的 effect runner。因此,當我們再去修改 state.count.a.b 的時候,就會通知這個 effect ,所以最終會執行 watcher 的回撥函數。
當我們偵聽一個通過 reactive API 建立的響應式物件時,內部會執行 traverse 函數,如果這個物件非常複雜,比如巢狀層級很深,那麼遞迴 traverse 就會有一定的效能耗時。因此如果我們需要偵聽這個複雜響應式物件內部的某個具體屬性,就可以想辦法減少 traverse 帶來的效能損耗。
比如剛才的例子,我們就可以直接偵聽 state.count.a.b 的變化:
watch(state.count.a, (newVal, oldVal) => { console.log(newVal) }) state.count.a.b = 2
這樣就可以減少內部執行 traverse 的次數。你可能會問,直接偵聽 state.count.a.b 可以嗎?答案是不行,因為 state.count.a.b 已經是一個基礎數位型別了,不符合 source 要求的引數型別,所以會在非生產環境下報警告。
那麼有沒有辦法優化使得 traverse 不執行呢?答案是可以的。我們可以偵聽一個 getter 函數:
watch(() => state.count.a.b, (newVal, oldVal) => { console.log(newVal) }) state.count.a.b = 2
這樣函數內部會存取並返回 state.count.a.b,一次 traverse 都不會執行並且依然可以偵聽到它的變化從而執行 watcher 的回撥函數。
處理完 watch API 第一個引數 source 後,接下來處理第二個引數 cb。
cb 是一個回撥函數,它有三個引數:第一個 newValue 代表新值;第二個 oldValue 代表舊值。第三個引數 onInvalidate,這個放在後面介紹。
其實這樣的 API 設計非常好理解,即偵聽一個值的變化,如果值變了就執行回撥函數,回撥函數裡可以存取到新值和舊值。
接下來我們來看一下構造回撥函數的處理邏輯:
let cleanup // 註冊無效回撥函數 const onInvalidate = (fn) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */) } } // 舊值初始值 let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE /*{}*/ // 回撥函數 const applyCb = cb ? () => { // 元件銷燬,則直接返回 if (instance && instance.isUnmounted) { return } // 求得新值 const newValue = runner() if (deep || hasChanged(newValue, oldValue)) { // 執行清理函數 if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [ newValue, // 第一次更改時傳遞舊值為 undefined oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onInvalidate ]) // 更新舊值 oldValue = newValue } } : void 0
onInvalidate 函數用來註冊無效回撥函數 ,我們暫時不需要關注它,我們需要重點來看 applyCb。 這個函數實際上就是對 cb 做一層封裝,當偵聽的值發生變化時就會執行 applyCb 方法,我們來分析一下它的實現。
首先,watch API 和元件範例相關,因為通常我們會在元件的 setup 函數中使用它,當元件銷燬後,回撥函數 cb 不應該被執行而是直接返回。
接著,執行 runner 求得新值,這裡實際上就是執行前面建立的 getter 函數求新值。
最後進行判斷,如果是 deep 的情況或者新舊值發生了變化,則執行回撥函數 cb,傳入引數 newValue 和 oldValue。注意,第一次執行的時候舊值的初始值是空陣列或者 undefined。執行完回撥函數 cb 後,把舊值 oldValue 再更新為 newValue,這是為了下一次的比對。
接下來我們要分析建立 scheduler 過程。
scheduler 的作用是根據某種排程的方式去執行某種函數,在 watch API 中,主要影響到的是回撥函數的執行方式。我們來看一下它的實現邏輯:
const invoke = (fn) => fn() let scheduler if (flush === 'sync') { // 同步 scheduler = invoke } else if (flush === 'pre') { scheduler = job => { if (!instance || instance.isMounted) { // 進入非同步佇列,元件更新前執行 queueJob(job) } else { // 如果元件還沒掛載,則同步執行確保在元件掛載前 job() } } } else { // 進入非同步佇列,元件更新後執行 scheduler = job => queuePostRenderEffect(job, instance && instance.suspense) }
Watch API 的引數除了 source 和 cb,還支援第三個引數 options,不同的設定決定了 watcher 的不同行為。前面我們也分析了 deep 為 true 的情況,除了 source 為 reactive 物件時會預設把 deep 設定為 true,你也可以主動傳入第三個引數,把 deep 設定為 true。
這裡,scheduler 的建立邏輯受到了第三個引數 Options 中的 flush 屬性值的影響,不同的 flush 決定了 watcher 的執行時機。
queueJob 和 queuePostRenderEffect 在這裡不是重點,所以我們放到後面介紹。總之,你現在要記住,watcher 的回撥函數是通過一定的排程方式執行的。
前面的分析我們提到了 runner,它其實就是 watcher 內部建立的 effect 函數,接下來,我們來分析它邏輯:
const runner = effect(getter, { // 延時執行 lazy: true, // computed effect 可以優先於普通的 effect 先執行,比如元件渲染的 effect computed: true, onTrack, onTrigger, scheduler: applyCb ? () => scheduler(applyCb) : scheduler }) // 在元件範例中記錄這個 effect recordInstanceBoundEffect(runner) // 初次執行 if (applyCb) { if (immediate) { applyCb() } else { // 求舊值 oldValue = runner() } } else { // 沒有 cb 的情況 runner() }
這塊程式碼邏輯是整個 watcher 實現的核心部分,即通過 effect API 建立一個副作用函數 runner,我們需要關注以下幾點。
runner 是一個 computed effect。 因為 computed effect 可以優先於普通的 effect(比如元件渲染的 effect)先執行,這樣就可以實現當設定 flush 為 pre 的時候,watcher 的執行可以優先於元件更新。
runner 執行的方式。 runner 是 lazy 的,它不會在建立後立刻執行。第一次手動執行 runner 會執行前面的 getter 函數,存取響應式資料並做依賴收集。注意,此時activeEffect 就是 runner,這樣在後面更新響應式資料時,就可以觸發 runner 執行 scheduler 函數,以一種排程方式來執行回撥函數。
runner 的返回結果。 手動執行 runner 就相當於執行了前面標準化的 getter 函數,getter 函數的返回值就是 watcher 計算出的值,所以我們第一次執行 runner 求得的值可以作為 oldValue。
設定了 immediate 的情況。 當我們設定了 immediate ,建立完 watcher 會立刻執行 applyCb 函數,此時 oldValue 還是初始值,在 applyCb 執行時也會執行 runner 進而執行前面的 getter 函數做依賴收集,求得新值。
最後,會返回偵聽器銷燬函數,也就是 watch API 執行後返回的函數。我們可以通過呼叫它來停止 watcher 對資料的偵聽。
return () => { stop(runner) if (instance) { // 移除元件 effects 對這個 runner 的參照 remove(instance.effects, runner) } } function stop(effect) { if (effect.active) { cleanup(effect) if (effect.options.onStop) { effect.options.onStop() } effect.active = false } }
銷燬函數內部會執行 stop 方法讓 runner 失活,並清理 runner 的相關依賴,這樣就可以停止對資料的偵聽。並且,如果是在元件中註冊的 watcher,也會移除元件 effects 對這個 runner 的參照。
偵聽器的回撥函數是以一種排程的方式執行的,特別是當 flush 不是 sync 時,它會把回撥函數執行的任務推到一個非同步佇列中執行。接下來,我們就來分析非同步執行佇列的設計。分析之前,我們先來思考一下,為什麼會需要非同步佇列?
我們把之前的例子簡單修改一下:
import { reactive, watch } from 'vue' const state = reactive({ count: 0 }) watch(() => state.count, (count, prevCount) => { console.log(count) }) state.count++ state.count++ state.count++
這裡,我們修改了三次 state.count,那麼 watcher 的回撥函數會執行三次嗎?
答案是不會,實際上只輸出了一次 count 的值,也就是最終計算的值 3。這在大多數場景下都是符合預期的,因為在一個 Tick(宏任務執行的生命週期)內,即使多次修改偵聽的值,它的回撥函數也只執行一次。
元件的更新過程是非同步的,我們知道修改模板中參照的響應式物件的值時,會觸發元件的重新渲染,但是在一個 Tick 內,即使你多次修改多個響應式物件的值,元件的重新渲染也只執行一次。這是因為如果每次更新資料都觸發元件重新渲染,那麼重新渲染的次數和代價都太高了。
那麼,這是怎麼做到的呢?我們先從非同步任務佇列的建立說起。
通過前面的分析我們知道,在建立一個 watcher 時,如果設定 flush 為 pre 或不設定 flush ,那麼 watcher 的回撥函數就會非同步執行。此時分別是通過 queueJob 和 queuePostRenderEffect 把回撥函數推入非同步佇列中的。
在不涉及 suspense 的情況下,queuePostRenderEffect 相當於 queuePostFlushCb,我們來看它們的實現:
// 非同步任務佇列 const queue = [] // 佇列任務執行完後執行的回撥函數佇列 const postFlushCbs = [] function queueJob(job) { if (!queue.includes(job)) { queue.push(job) queueFlush() } } function queuePostFlushCb(cb) { if (!isArray(cb)) { postFlushCbs.push(cb) } else { // 如果是陣列,把它拍平成一維 postFlushCbs.push(...cb) } queueFlush() }
Vue.js 內部維護了一個 queue 陣列和一個 postFlushCbs 陣列,其中 queue 陣列用作非同步任務佇列, postFlushCbs 陣列用作非同步任務佇列執行完畢後的回撥函數佇列。
執行 queueJob 時會把這個任務 job 新增到 queue 的隊尾,而執行 queuePostFlushCb 時,會把這個 cb 回撥函數新增到 postFlushCbs 的隊尾。它們在新增完畢後都執行了 queueFlush 函數,我們接著看它的實現:
const p = Promise.resolve() // 非同步任務佇列是否正在執行 let isFlushing = false // 非同步任務佇列是否等待執行 let isFlushPending = false function nextTick(fn) { return fn ? p.then(fn) : p } function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true nextTick(flushJobs) } }
可以看到,Vue.js 內部還維護了 isFlushing 和 isFlushPending 變數,用來控制非同步任務的重新整理邏輯。
在 queueFlush 首次執行時,isFlushing 和 isFlushPending 都是 false,此時會把 isFlushPending 設定為 true,並且呼叫 nextTick(flushJobs) 去執行佇列裡的任務。
因為 isFlushPending 的控制,這使得即使多次執行 queueFlush,也不會多次去執行 flushJobs。另外 nextTick 在 Vue.js 3.0 中的實現也是非常簡單,通過 Promise.resolve().then 去非同步執行 flushJobs。
因為 JavaScript 是單執行緒執行的,這樣的非同步設計使你在一個 Tick 內,可以多次執行 queueJob 或者 queuePostFlushCb 去新增任務,也可以保證在宏任務執行完畢後的微任務階段執行一次 flushJobs。
建立完任務佇列後,接下來要非同步執行這個佇列,我們來看一下 flushJobs 的實現:
const getId = (job) => (job.id == null ? Infinity : job.id) function flushJobs(seen) { isFlushPending = false isFlushing = true let job if ((process.env.NODE_ENV !== 'production')) { seen = seen || new Map() } // 元件的更新是先父後子 // 如果一個元件在父元件更新過程中解除安裝,它自身的更新應該被跳過 queue.sort((a, b) => getId(a) - getId(b)) while ((job = queue.shift()) !== undefined) { if (job === null) { continue } if ((process.env.NODE_ENV !== 'production')) { checkRecursiveUpdates(seen, job) } callWithErrorHandling(job, null, 14 /* SCHEDULER */) } flushPostFlushCbs(seen) isFlushing = false // 一些 postFlushCb 執行過程中會再次新增非同步任務,遞迴 flushJobs 會把它們都執行完畢 if (queue.length || postFlushCbs.length) { flushJobs(seen) } }
可以看到,flushJobs 函數開始執行的時候,會把 isFlushPending 重置為 false,把 isFlushing 設定為 true 來表示正在執行非同步任務佇列。
對於非同步任務佇列 queue,在遍歷執行它們前會先對它們做一次從小到大的排序,這是因為兩個主要原因:
接下來,就是遍歷這個 queue,依次執行佇列中的任務了,在遍歷過程中,注意有一個 checkRecursiveUpdates 的邏輯,它是用來在非生產環境下檢測是否有迴圈更新的,它的作用我們稍後會提。
遍歷完 queue 後,又會進一步執行 flushPostFlushCbs 方法去遍歷執行所有推入到 postFlushCbs 的回撥函數:
function flushPostFlushCbs(seen) { if (postFlushCbs.length) { // 拷貝副本 const cbs = [...new Set(postFlushCbs)] postFlushCbs.length = 0 if ((process.env.NODE_ENV !== 'production')) { seen = seen || new Map() } for (let i = 0; i < cbs.length; i++) { if ((process.env.NODE_ENV !== 'production')) { checkRecursiveUpdates(seen, cbs[i]) } cbs[i]() } } }
注意這裡遍歷前會通過 const cbs = [...new Set(postFlushCbs)] 拷貝一個 postFlushCbs 的副本,這是因為在遍歷的過程中,可能某些回撥函數的執行會再次修改 postFlushCbs,所以拷貝一個副本回圈遍歷則不會受到 postFlushCbs 修改的影響。
遍歷完 postFlushCbs 後,會重置 isFlushing 為 false,因為一些 postFlushCb 執行過程中可能會再次新增非同步任務,所以需要繼續判斷如果 queue 或者 postFlushCbs 佇列中還存在任務,則遞迴執行 flushJobs 把它們都執行完畢。
前面我們提到了,在遍歷執行非同步任務和回撥函數的過程中,都會在非生產環境下執行 checkRecursiveUpdates 檢測是否有迴圈更新,它是用來解決什麼問題的呢?
我們把之前的例子改寫一下:
import { reactive, watch } from 'vue' const state = reactive({ count: 0 }) watch(() => state.count, (count, prevCount) => { state.count++ console.log(count) }) state.count++
如果你去跑這個範例,你會在控制檯看到輸出了 101 次值,然後報了錯誤: Maximum recursive updates exceeded 。這是因為我們在 watcher 的回撥函數裡更新了資料,這樣會再一次進入回撥函數,如果我們不加任何控制,那麼回撥函數會一直執行,直到把記憶體耗盡造成瀏覽器假死。
為了避免這種情況,Vue.js 實現了 checkRecursiveUpdates 方法:
const RECURSION_LIMIT = 100 function checkRecursiveUpdates(seen, fn) { if (!seen.has(fn)) { seen.set(fn, 1) } else { const count = seen.get(fn) if (count > RECURSION_LIMIT) { throw new Error('Maximum recursive updates exceeded. ' + "You may have code that is mutating state in your component's " + 'render function or updated hook or watcher source function.') } else { seen.set(fn, count + 1) } } }
通過前面的程式碼,我們知道 flushJobs 一開始便建立了 seen,它是一個 Map 物件,然後在 checkRecursiveUpdates 的時候會把任務新增到 seen 中,記錄參照計數 count,初始值為 1,如果 postFlushCbs 再次新增了相同的任務,則參照計數 count 加 1,如果 count 大於我們定義的限制 100 ,則說明一直在新增這個相同的任務並超過了 100 次。那麼,Vue.js 會丟擲這個錯誤,因為在正常的使用中,不應該出現這種情況,而我們上述的錯誤範例就會觸發這種報錯邏輯。
到這裡,非同步佇列的設計就介紹完畢了,你可能會對 isFlushPending 和 isFlushing 有些疑問,為什麼需要兩個變數來控制呢?
從語意上來看,isFlushPending 用於判斷是否在等待 nextTick 執行 flushJobs,而 isFlushing 是判斷是否正在執行任務佇列。
從功能上來看,它們的作用是為了確保以下兩點:
但實際上,這裡我們可以進行優化。在我看來,這裡用一個變數就足夠了,我們來稍微修改一下原始碼:
function queueFlush() { if (!isFlushing) { isFlushing = true nextTick(flushJobs) } } function flushJobs(seen) { let job if ((process.env.NODE_ENV !== 'production')) { seen = seen || new Map() } queue.sort((a, b) => getId(a) - getId(b)) while ((job = queue.shift()) !== undefined) { if (job === null) { continue } if ((process.env.NODE_ENV !== 'production')) { checkRecursiveUpdates(seen, job) } callWithErrorHandling(job, null, 14 /* SCHEDULER */) } flushPostFlushCbs(seen) if (queue.length || postFlushCbs.length) { flushJobs(seen) } isFlushing = false }
可以看到,我們只需要一個 isFlushing 來控制就可以實現相同的功能了。在執行 queueFlush 的時候,判斷 isFlushing 為 false,則把它設定為 true,然後 nextTick 會執行 flushJobs。在 flushJobs 函數執行完成的最後,也就是所有的任務(包括後新增的)都執行完畢,再設定 isFlushing 為 false。
瞭解完 watch API 和非同步任務佇列的設計後,我們再來學習偵聽器提供的另一個 API—— watchEffect API。
watchEffect API 的作用是註冊一個副作用函數,副作用函數內部可以存取到響應式物件,當內部響應式物件變化後再立即執行這個函數。
可以先來看一個範例:
import { ref, watchEffect } from 'vue' const count = ref(0) watchEffect(() => console.log(count.value)) count.value++
它的結果是依次輸出 0 和 1。
watchEffect 和前面的 watch API 有哪些不同呢?主要有三點:
對 watchEffect API 有大體瞭解後,我們來看一下在我整理的 watchEffect 場景下, doWatch 函數的簡化版實現:
function watchEffect(effect, options) { return doWatch(effect, null, options); } function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) { instance = currentInstance; let getter; if (isFunction(source)) { getter = () => { if (instance && instance.isUnmounted) { return; } // 執行清理函數 if (cleanup) { cleanup(); } // 執行 source 函數,傳入 onInvalidate 作為引數 return callWithErrorHandling(source, instance, 3 /* WATCH_CALLBACK */, [onInvalidate]); }; } let cleanup; const onInvalidate = (fn) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); }; }; let scheduler; // 建立 scheduler if (flush === 'sync') { scheduler = invoke; } else if (flush === 'pre') { scheduler = job => { if (!instance || instance.isMounted) { queueJob(job); } else { job(); } }; } else { scheduler = job => queuePostRenderEffect(job, instance && instance.suspense); } // 建立 runner const runner = effect(getter, { lazy: true, computed: true, onTrack, onTrigger, scheduler }); recordInstanceBoundEffect(runner); // 立即執行 runner runner(); // 返回銷燬函數 return () => { stop(runner); if (instance) { remove(instance.effects, runner); } }; }
可以看到,getter 函數就是對 source 函數的簡單封裝,它會先判斷元件範例是否已經銷燬,然後每次執行 source 函數前執行 cleanup 清理函數。
watchEffect 內部建立的 runner 對應的 scheduler 物件就是 scheduler 函數本身,這樣它再次執行時,就會執行這個 scheduler 函數,並且傳入 runner 函數作為引數,其實就是按照一定的排程方式去執行基於 source 封裝的 getter 函數。
建立完 runner 後就立刻執行了 runner,其實就是內部同步執行了基於 source 封裝的 getter 函數。
在執行 source 函數的時候,會傳入一個 onInvalidate 函數作為引數,接下來我們就來分析它的作用。
有些時候,watchEffect 會註冊一個副作用函數,在函數內部可以做一些非同步操作,但是當這個 watcher 停止後,如果我們想去對這個非同步操作做一些額外事情(比如取消這個非同步操作),我們可以通過 onInvalidate 引數註冊一個無效函數。
import {ref, watchEffect } from 'vue' const id = ref(0) watchEffect(onInvalidate => { // 執行非同步操作 const token = performAsyncOperation(id.value) onInvalidate(() => { // 如果 id 發生變化或者 watcher 停止了,則執行邏輯取消前面的非同步操作 token.cancel() }) })
我們利用 watchEffect 註冊了一個副作用函數,它有一個 onInvalidate 引數。在這個函數內部通過 performAsyncOperation 執行某些非同步操作,並且存取了 id 這個響應式物件,然後通過 onInvalidate 註冊了一個回撥函數。
如果 id 發生變化或者 watcher 停止了,這個回撥函數將會執行,然後執行 token.cancel 取消之前的非同步操作。
我們來回顧 onInvalidate 在 doWatch 中的實現:
const onInvalidate = (fn) => { cleanup = runner.options.onStop = () => { callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */); }; };
實際上,當你執行 onInvalidate 的時候,就是註冊了一個 cleanup 和 runner 的 onStop 方法,這個方法內部會執行 fn,也就是你註冊的無效回撥函數。
也就是說當響應式資料發生變化,會執行 cleanup 方法,當 watcher 被停止,會執行 onStop 方法,這兩者都會執行註冊的無效回撥函數 fn。
通過這種方式,Vue.js 就很好地實現了 watcher 註冊無效回撥函數的需求。
偵聽器的內部設計很巧妙,我們可以偵聽響應式資料的變化,內部建立 effect runner,首次執行 runner 做依賴收集,然後在資料發生變化後,以某種排程方式去執行回撥函數。
相比於計算屬性,偵聽器更適合用於在資料變化後執行某段邏輯的場景,而計算屬性則用於一個資料依賴另外一些資料計算而來的場景。
以上就是深入瞭解Vue3中偵聽器watcher的實現原理的詳細內容,更多關於Vue3 偵聽器watcher的資料請關注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