一文詳解Vue3的watch是如何實現(xiàn)監(jiān)聽的
前言
watch這個API大家都很熟悉,今天這篇文章歐陽來帶你搞清楚Vue3的watch是如何實現(xiàn)對響應(yīng)式數(shù)據(jù)進行監(jiān)聽的。注:本文使用的Vue版本為3.5.13。
看個demo
我們來看個簡單的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>
這個demo很簡單,使用watch監(jiān)聽了響應(yīng)式變量count,在watch回調(diào)中進行了console打印。如何有個button按鈕,點擊后會count++。
開始打斷點
現(xiàn)在我們第一個斷點應(yīng)該打在哪里呢?
我們要看watch的實現(xiàn),那么當(dāng)然是給我們demo中的watch函數(shù)打個斷點。
首先執(zhí)行yarn dev將我們的demo跑起來,然后在瀏覽器的network面板中找到對應(yīng)的vue文件,右鍵點擊Open in Sources panel就可以在source面板中打開我們的代碼啦。如下圖

然后給watch函數(shù)打個斷點,如下圖:

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

