一文詳解Vue3的watch是如何實(shí)現(xiàn)監(jiān)聽的
前言
watch這個(gè)API大家都很熟悉,今天這篇文章歐陽(yáng)來帶你搞清楚Vue3的watch是如何實(shí)現(xiàn)對(duì)響應(yīng)式數(shù)據(jù)進(jìn)行監(jiān)聽的。注:本文使用的Vue版本為3.5.13
。
看個(gè)demo
我們來看個(gè)簡(jiǎn)單的demo,代碼如下:
<template> <button @click="count++">count++</button> </template> <script setup lang="ts"> import { ref, watch } from "vue"; const count = ref(0); watch(count, (preVal, curVal) => { console.log("count is changed", preVal, curVal); }); </script>
這個(gè)demo很簡(jiǎn)單,使用watch監(jiān)聽了響應(yīng)式變量count
,在watch回調(diào)中進(jìn)行了console打印。如何有個(gè)button按鈕,點(diǎn)擊后會(huì)count++。
開始打斷點(diǎn)
現(xiàn)在我們第一個(gè)斷點(diǎn)應(yīng)該打在哪里呢?
我們要看watch的實(shí)現(xiàn),那么當(dāng)然是給我們demo中的watch函數(shù)打個(gè)斷點(diǎn)。
首先執(zhí)行yarn dev
將我們的demo跑起來,然后在瀏覽器的network面板中找到對(duì)應(yīng)的vue文件,右鍵點(diǎn)擊Open in Sources panel就可以在source面板中打開我們的代碼啦。如下圖
然后給watch函數(shù)打個(gè)斷點(diǎn),如下圖:
接著刷新頁(yè)面,此時(shí)代碼將會(huì)停留在斷點(diǎn)出。將斷點(diǎn)走進(jìn)watch函數(shù),代碼如下:
function watch(source, cb, options) { return doWatch(source, cb, options); }
從上面的代碼可以看到在watch函數(shù)中直接返回了doWatch
函數(shù)。
將斷點(diǎn)走進(jìn)doWatch
函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下(為了方便大家理解,本文中會(huì)將scheduler任務(wù)調(diào)度相關(guān)的代碼移除掉,因?yàn)檫@個(gè)不影響watch的主流程):
function doWatch(source, cb, options = EMPTY_OBJ) { const baseWatchOptions = extend({}, options); const watchHandle = baseWatch(source, cb, baseWatchOptions); return watchHandle; }
從上面的代碼可以看到底層實(shí)際是在執(zhí)行baseWatch
函數(shù),而這個(gè)baseWatch
就是由@vue/reactivity
包中導(dǎo)出的watch函數(shù)。關(guān)于這個(gè)baseWatch
函數(shù)的由來可以看看歐陽(yáng)之前的文章: Vue3.5新增的baseWatch讓watch函數(shù)和Vue組件徹底分手
baseWatch函數(shù)
將斷點(diǎn)走進(jìn)baseWatch
函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:
const INITIAL_WATCHER_VALUE = {} function watch( source: WatchSource | WatchSource[] | WatchEffect | object, cb?: WatchCallback | null, options: WatchOptions = EMPTY_OBJ ): WatchHandle { let effect: ReactiveEffect; let getter: () => any; if (isRef(source)) { getter = () => source.value; } let oldValue: any = INITIAL_WATCHER_VALUE; const job = () => { if (cb) { const newValue = effect.run(); if (hasChanged(newValue, oldValue)) { const args = [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, boundCleanup, ]; cb(...args); oldValue = newValue; } } }; effect = new ReactiveEffect(getter); effect.scheduler = job; oldValue = effect.run(); }
首先定義了兩個(gè)變量effect
和getter
,effect
是ReactiveEffect
類的實(shí)例。
接著就是使用isRef(source)
判斷watch監(jiān)聽的是不是一個(gè)ref變量,如果是就將getter
函數(shù)賦值為getter = () => source.value
。這么做的原因是為了保持一致(watch也可以直接監(jiān)聽一個(gè)getter函數(shù)),并且后面會(huì)對(duì)這個(gè)getter函數(shù)進(jìn)行讀操作觸發(fā)依賴收集。
我們知道watch的回調(diào)中有oldValue
和newValue
這兩個(gè)字段,在watch
函數(shù)內(nèi)部有個(gè)字段也名為oldValue
用于存舊的值。
接著就是定義了一個(gè)job
函數(shù),我們先不看里面的代碼,執(zhí)行這個(gè)job
函數(shù)就會(huì)執(zhí)行watch的回調(diào)。
然后執(zhí)行effect = new ReactiveEffect(getter)
,這個(gè)ReactiveEffect
類是一個(gè)底層的類。在Vue的設(shè)計(jì)中,所有的訂閱者都是繼承的這個(gè)ReactiveEffect
類。比如watchEffect、computed()、render函數(shù)等。
在我們這個(gè)場(chǎng)景中new ReactiveEffect
時(shí)傳入的getter
函數(shù)就是getter = () => source.value
,這里的source
就是watch監(jiān)聽的響應(yīng)式變量count
。
接著將job
函數(shù)賦值給effect.scheduler
屬性,在ReactiveEffect
類中依賴觸發(fā)時(shí)就會(huì)執(zhí)行effect.scheduler
方法(接下來會(huì)講)。
最后就是執(zhí)行effect.run()
拿到初始化時(shí)watch監(jiān)聽變量的值,這個(gè)run
方法也是在ReactiveEffect
類中。接下來也會(huì)講。
ReactiveEffect類
前面我們講過了ReactiveEffect
是Vue的一個(gè)底層類,所有的訂閱者都是繼承的這個(gè)類。將斷點(diǎn)走進(jìn)ReactiveEffect
類,在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:
class ReactiveEffect<T = any> implements Subscriber, ReactiveEffectOptions { constructor(fn) { this.fn = fn; } run(): T { const prevEffect = activeSub; activeSub = this; try { return this.fn(); } finally { activeSub = prevEffect; } } trigger(): void { this.scheduler(); } }
在new一個(gè)ReactiveEffect
實(shí)例時(shí)傳入的getter函數(shù)會(huì)賦值給實(shí)例的fn
方法。(實(shí)際的ReactiveEffect
代碼比這個(gè)要復(fù)雜很多,感興趣的同學(xué)可以去看源代碼)
我們回到前面講過的baseWatch
函數(shù)中的最后一塊:oldValue = effect.run()
。這里執(zhí)行了effect
實(shí)例的run
方法拿到watch監(jiān)聽變量的值,并且賦值給oldValue
變量。
因?yàn)槲覀內(nèi)绻皇褂?code>immediate: true,那么Vue會(huì)等watch監(jiān)聽的變量改變后才會(huì)觸發(fā)watch回調(diào),回調(diào)中有個(gè)字段叫oldValue
,這個(gè)oldValue
就是初始化時(shí)執(zhí)行run
方法拿到的。
比如我們這里count
初始化的值是0,初始化執(zhí)行oldValue = effect.run()
后就會(huì)給oldValue
賦值為0。當(dāng)點(diǎn)擊count++按鈕后,count
的值就變成了1,所以在watch回調(diào)第一次觸發(fā)的時(shí)候他就知道oldValue
的值是0啦。
除此之外,在run
方法中還有收集依賴的作用。Vue維護(hù)了一個(gè)全局變量activeSub
表示當(dāng)前active的訂閱者是誰(shuí),在同一時(shí)間只可能有一個(gè)active的訂閱者,不然觸發(fā)get攔截進(jìn)行依賴收集時(shí)就不知道該把哪個(gè)訂閱者給收集了。
在run
方法中將當(dāng)前的activeSub
給存起來,等下面的代碼執(zhí)行完了后將全局變量activeSub
改回去。
接著就是執(zhí)行activeSub = this;
將當(dāng)前的watch設(shè)置為全局變量activeSub
。
接下來就是執(zhí)行return this.fn()
,前面我們講過了這個(gè)this.fn()
方法就是watch監(jiān)聽的getter函數(shù)。由于我們watch監(jiān)聽的是一個(gè)響應(yīng)式變量count
,在前面處理后他的getter函數(shù)就是getter = () => source.value;
。這里的source就是watch監(jiān)聽的變量,這個(gè)getter函數(shù)實(shí)際就是getter = () => count.value;
那么這里執(zhí)行return this.fn()
就是執(zhí)行() => count.value
,將會(huì)觸發(fā)響應(yīng)式變量count
的get攔截。在get攔截中會(huì)進(jìn)行依賴收集,由于此時(shí)的全局變量activeSub
已經(jīng)變成了訂閱者watch,所以響應(yīng)式變量count
在依賴收集的過程中收集的訂閱者就是watch。這樣響應(yīng)式變量count
就和訂閱者watch建立了依賴收集的關(guān)系。關(guān)于Vue3.5依賴收集和依賴觸發(fā)可以看看歐陽(yáng)之前的文章: 看不懂來打我!讓性能提升56%的Vue3.5響應(yīng)式重構(gòu)
當(dāng)我們點(diǎn)擊count++后會(huì)修改響應(yīng)式變量count
的值,就會(huì)進(jìn)行依賴觸發(fā),經(jīng)過一堆操作后最后就會(huì)執(zhí)行到這里的trigger
方法中。在trigger
方法中直接執(zhí)行this.scheduler()
,在前面已經(jīng)對(duì)scheduler
方法進(jìn)行了賦值,回憶一下baseWatch
函數(shù)的代碼。如下:
const INITIAL_WATCHER_VALUE = {} function watch( source: WatchSource | WatchSource[] | WatchEffect | object, cb?: WatchCallback | null, options: WatchOptions = EMPTY_OBJ ): WatchHandle { let effect: ReactiveEffect; let getter: () => any; if (isRef(source)) { getter = () => source.value; } let oldValue: any = INITIAL_WATCHER_VALUE; const job = () => { if (cb) { const newValue = effect.run(); if (hasChanged(newValue, oldValue)) { const args = [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, boundCleanup, ]; cb(...args); oldValue = newValue; } } }; effect = new ReactiveEffect(getter); effect.scheduler = job; oldValue = effect.run(); }
這里將job
函數(shù)賦值給effect.scheduler
方法,所以當(dāng)響應(yīng)式變量count
的值改變后實(shí)際就是在執(zhí)行這里的job
函數(shù)。
在job
函數(shù)中首先判斷是否有傳入watch的callback函數(shù),然后執(zhí)行const newValue = effect.run()
。
執(zhí)行這行代碼有兩個(gè)作用:
第一個(gè)作用是重新執(zhí)行g(shù)etter函數(shù),也就是getter = () => count.value;
,拿到最新count
的值,將其賦值給newValue
。
第二個(gè)作用是watch除了監(jiān)聽響應(yīng)式變量之外還可以監(jiān)聽一個(gè)getter函數(shù),那么在getter
函數(shù)中就可以類似computed一樣在某些條件下監(jiān)聽變量A,某些條件下監(jiān)聽變量B。這里的第二個(gè)作用是重新收集依賴,因?yàn)榇藭r(shí)watch可能從監(jiān)聽變量A變成了監(jiān)聽變量B。
接著就是執(zhí)行if (hasChanged(newValue, oldValue))
判斷watch監(jiān)聽的變量新的值和舊的值是否相等,如果不相等才去執(zhí)行cb(...args)
觸發(fā)watch的回調(diào)。最后就是將當(dāng)前的newValue
賦值給oldValue
,下次觸發(fā)watch回調(diào)時(shí)作為oldValue
字段。
總結(jié)
這篇文章講了watch如何對(duì)響應(yīng)式變量進(jìn)行監(jiān)聽,其實(shí)底層依賴的是@vue/reactivity
包的baseWatch
函數(shù)。在baseWatch
函數(shù)中會(huì)使用ReactiveEffect
類new一個(gè)effect
實(shí)例,這個(gè)ReactiveEffect
類是一個(gè)底層的類,Vue的訂閱者都是基于這個(gè)類去實(shí)現(xiàn)的。
如果沒有使用immediate: true
,初始化時(shí)會(huì)去執(zhí)行一次effect.run()
對(duì)watch監(jiān)聽的響應(yīng)式變量進(jìn)行讀操作并且將其賦值給oldValue
。讀操作會(huì)觸發(fā)get攔截進(jìn)行響應(yīng)式變量的依賴收集,會(huì)將當(dāng)前watch作為訂閱者進(jìn)行收集。
當(dāng)響應(yīng)式變量的值改變后會(huì)觸發(fā)set攔截,進(jìn)而依賴觸發(fā)。前一步將watch也作為訂閱者進(jìn)行了收集,依賴觸發(fā)時(shí)也會(huì)通知到watch,所以此時(shí)會(huì)執(zhí)行watch中的job
函數(shù)。在job
函數(shù)中會(huì)再次執(zhí)行effect.run()
拿到響應(yīng)式變量最新的值賦值給newValue
,同時(shí)再次進(jìn)行依賴收集。如果oldValue
和newValue
不相等,那么就觸發(fā)watch的回調(diào),并且將oldValue
和newValue
作為參數(shù)傳過去。
到此這篇關(guān)于一文詳解Vue3的watch是如何實(shí)現(xiàn)監(jiān)聽的的文章就介紹到這了,更多相關(guān)Vue3 watch實(shí)現(xiàn)監(jiān)聽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue.js實(shí)現(xiàn)按鈕的動(dòng)態(tài)綁定效果及實(shí)現(xiàn)代碼
本文通過實(shí)例代碼給大家介紹了Vue.js實(shí)現(xiàn)按鈕的動(dòng)態(tài)綁定效果,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的的朋友參考下吧2017-08-08vue3-pinia-ts項(xiàng)目中的使用示例詳解
這篇文章主要介紹了vue3-pinia-ts項(xiàng)目中的使用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08vue3中emit('update:modelValue')使用簡(jiǎn)單示例
這篇文章主要給大家介紹了關(guān)于vue3中emit('update:modelValue')使用的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-09-09富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能
這篇文章主要介紹了富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05關(guān)于vue.js組件數(shù)據(jù)流的問題
本篇文章主要介紹了關(guān)于vue.js組件數(shù)據(jù)流的問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07Vue2中無法監(jiān)聽數(shù)組和對(duì)象的某些變化問題
這篇文章主要介紹了Vue2中無法監(jiān)聽數(shù)組和對(duì)象的某些變化問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08