前言
watch這個API大家應該都不陌生,在Vue3版本中給watch增加不少有用的功能,比如deep選項支援傳入數字
、pause、resume、stop方法
、once選項
、onCleanup函式
。這些功能大家平時都不怎麼用得上,但是在一些特定的場景中,他們能夠起大作用,這篇文章歐陽就來帶你盤點一下這些功能。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
deep
支援傳入數字
deep
選項大家應該比較熟悉,常見的值為true
或者false
,表示是否深度監聽watch
傳入的物件。
在Vue3.5版本中對deep
選項進行了增強,不光支援布林值,而且還支援傳入數字,數字表示需要監聽的層數。
比如下面這個例子:
const obj1 = ref({
a: {
b: 1,
c: {
d: 2,
e: {
f: 3,
},
},
},
});
watch(
obj1,
() => {
console.log("監聽到obj1變化");
},
{
deep: 3,
}
);
function changeDeep3Obj() {
obj1.value.a.c.d = 20; // 能夠觸發watch回撥
}
function changeDeep4Obj() {
obj1.value.a.c.e.f = 30; // 不能觸發watch回撥
}
在上面的例子watch
的deep
選項值是3,表明監聽到物件的第3層。
changeDeep3Obj
函式中就是修改物件的第3層的d
屬性,所以能夠觸發watch
的回撥。
而changeDeep4Obj
函式是修改物件的第4層的f
屬性,所以不能觸發watch
的回撥。
他的實現也很簡單,我們來看一下deep相關的原始碼:
function watch(source, cb, options) {
// ...省略
if (cb && deep) {
const depth = deep === true ? Infinity : deep
getter = () => traverse(baseGetter(), depth)
}
// ...省略
}
這裡的depth
就表示watch監聽一個物件的深度。
如果deep
選項的值為true,那麼就將depth
設定為正無窮Infinity
,說明需要監聽到物件的最深處。
如果deep
選項的值為false,或者沒有傳入deep
,那麼就表明只需要監聽物件的最外層。
如果deep
選項的值為number型別數字,那麼就把這個數字賦給depth
,表明需要監聽到物件的具體某一層。
pause、resume、stop方法
這三個方法也是Vue3.5版本中引入的,透過解構watch
函式的返回值就可以直接拿到pause
、resume
、stop
這三個方法。
我們來看一下原始碼,其實很簡單:
function watch(source, cb, options) {
// ...省略
watchHandle.pause = effect.pause.bind(effect)
watchHandle.resume = effect.resume.bind(effect)
watchHandle.stop = watchHandle
return watchHandle
}
watch返回了一個名為watchHandle
的物件,物件上面有pause、resume、stop
這三個方法,所以我們可以透過解構watch
函式的返回值拿到這三個方法。
pause
方法的作用是“暫停”watch回撥的觸發,也就是說在暫停期間不管watch監聽的響應式變數如何改變,他的回撥函式都不會觸發。
有“暫停”,那麼肯定就有“恢復”。
resume
方法的作用是恢復watch回撥的觸發,此時會主動執行一次watch的回撥。後面watch監聽的響應式變數改變時,他的回撥函式也會觸發。
來看個demo,程式碼如下:
<template>
<button @click="count++">count++</button>
<button @click="runner.pause()">暫停</button>
<button @click="runner.resume()">恢復</button>
<button @click="runner.stop()">停止</button>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
const count = ref(0);
const runner = watch(count, () => {
console.log(count.value);
});
</script>
點選“count++”按鈕會導致watch
回撥中的console執行。
但是當我們點選了“暫停”按鈕後,此時我們再怎麼點選“count++”按鈕都不會觸發watch
的回撥。
點選恢復
按鈕後會立即觸發一次watch
回撥的執行,後面點選“count++”按鈕也同樣會觸發watch
的回撥。
我們來看看pause
和resume
方法的原始碼,很簡單,程式碼如下:
class ReactiveEffect {
pause(): void {
this.flags |= EffectFlags.PAUSED
}
resume(): void {
if (this.flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this)
this.trigger()
}
}
}
trigger(): void {
if (this.flags & EffectFlags.PAUSED) {
pausedQueueEffects.add(this)
} else if (this.scheduler) {
this.scheduler()
} else {
this.runIfDirty()
}
}
}
在pause
、resume
方法中透過修改flags
屬性的值,來切換是不是“暫停狀態”。
在執行trigger
方法依賴觸發時,就會先去讀取flags
屬性判斷當前是不是“暫停狀態”,如果是那麼就不去執行watch的回撥。
從上面的程式碼可以看到這三個方法是在ReactiveEffect
類上面的,這個ReactiveEffect
類是Vue的一個底層類,watch
、watchEffect
、watchPosEffect
、watchSyncEffect
都是基於這個類實現的,所以他們自然也支援pause
、resume
、stop
這三個方法。
最後就是stop
方法了,當你確定後面都不再想要觸發watch的回撥了,那麼就呼叫這個stop
方法。程式碼如下:
const watchHandle: WatchHandle = () => {
effect.stop()
if (scope && scope.active) {
remove(scope.effects, effect)
}
}
watchHandle.stop = watchHandle
響應式變數count
收集的訂閱者集合中有這個watch回撥,所以當count
的值改變後會觸發watch回撥。這裡的stop
方法中主要是依靠雙向連結串列將這個watch回撥從響應式變數count
的訂閱者集合中給remove掉,所以執行stop方法後無論count
變數的值如何改變,watch回撥也不會再執行了。(PS:如果你看不懂這段話,建議你去看看我的上一篇 Vue3.5雙向連結串列文章,看完後你就懂了)
once選項
如果你只想讓你的watch回撥只執行一次,那麼可以試試這個once
選項,這個是在Vue3.4版本中新加的。
看個demo:
<template>
<button @click="count++">count++</button>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
const count = ref(0);
watch(
count,
() => {
console.log("once", count.value);
},
{
once: true,
}
);
</script>
由於使用了once
選項,所以只有第一次點選“count++”按鈕才會觸發watch的回撥。後面再怎麼點選按鈕都不會觸發watch回撥。
我們來看看once
選項的原始碼,很簡單,程式碼如下:
function watch(source, cb, options) {
const watchHandle: WatchHandle = () => {
effect.stop()
if (scope && scope.active) {
remove(scope.effects, effect)
}
}
if (once && cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
watchHandle()
}
}
// ...省略
watchHandle.pause = effect.pause.bind(effect)
watchHandle.resume = effect.resume.bind(effect)
watchHandle.stop = watchHandle
return watchHandle
}
先看中間的程式碼if (once && cb)
,這句話的意思是如果once
選項的值為true,並且也傳入了watch回撥。那麼就封裝一層新的cb
回撥函式,在新的回撥函式中還是會執行使用者傳入的watch回撥。然後再去執行一個watchHandle
函式,這個watchHandle
是不是覺得有點眼熟?
前面講的stop
方法其實就是在執行這個watchHandle
,執行完這個watchHandle
函式後watch就不再監聽count
變數了,所以後續不管count
變數怎麼修改,watch的回撥也不會再觸發。
onCleanup函式
有的情況我們需要watch監聽一個變數,然後去發起http請求。如果變數改變的很快就會出現第一個請求還沒回來,第二個請求就已經發起了。在一些極端情況下還會出現第一個請求的響應比第二個請求的響應還要慢,此時第一個請求的返回值就會覆蓋第二個請求的返回值。實際上我們期待最終拿到的是第二個請求的返回值。
這種情況我們就可以使用onCleanup函式
,他是作為watch回撥的第三個引數暴露給我們的。看個例子:
watch(id, async (newId, oldId, onCleanup) => {
const { response, cancel } = myFetch(newId)
// 當 `id` 變化時,`cancel` 將被呼叫,
// 取消之前的未完成的請求
onCleanup(cancel)
data.value = await response
})
watch回撥的前兩個引數大家都很熟悉:新的id值和舊的id值。第三個引數就是onCleanup
函式,在watch回撥觸發之前呼叫,所以我們可以使用他來cancel掉上一次的請求。
onCleanup
函式的註冊也很簡單,程式碼如下:
let boundCleanup
boundCleanup = fn => onWatcherCleanup(fn, false, effect)
function watch(source, cb, options) {
// ...省略
const job = (immediateFirstRun?: boolean) => {
const args = [
newValue,
oldValue,
boundCleanup,
]
cb(...args)
oldValue = newValue
}
// ...省略
}
執行watch回撥實際就是在執行這個job
函式,在job
函式中執行watch回撥時傳入了三個引數。分別是newValue
、oldValue
、boundCleanup
。前兩個引數大家都很熟悉,第三個引數boundCleanup
是一個函式:fn => onWatcherCleanup(fn, false, effect)
。
這個onWatcherCleanup
大家熟悉不?這也是Vue暴露出來的一個API,註冊一個清理函式,在當前偵聽器即將重新執行時執行。關於onWatcherCleanup
之前歐陽寫過一篇文章專門講了如何使用: 使用Vue3.5的onWatcherCleanup封裝自動cancel的fetch函式
總結
這篇文章盤點了Vue3 watch新增的一些新功能:deep選項支援傳入數字
、pause、resume、stop方法
、once選項
、onCleanup函式
。這些功能大家平時可能用不上,但是還是要知道有這些功能,因為有的情況下這些功能能夠派上大用場。
關注公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。