盤點Vue3 watch的一些關鍵時刻能夠大顯身手的功能

前端欧阳發表於2024-11-21

前言

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回撥
}

在上面的例子watchdeep選項值是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函式的返回值就可以直接拿到pauseresumestop這三個方法。

我們來看一下原始碼,其實很簡單:

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的回撥。

我們來看看pauseresume方法的原始碼,很簡單,程式碼如下:

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()
    }
  }
}

pauseresume方法中透過修改flags屬性的值,來切換是不是“暫停狀態”。

在執行trigger方法依賴觸發時,就會先去讀取flags屬性判斷當前是不是“暫停狀態”,如果是那麼就不去執行watch的回撥。

從上面的程式碼可以看到這三個方法是在ReactiveEffect類上面的,這個ReactiveEffect類是Vue的一個底層類,watchwatchEffectwatchPosEffectwatchSyncEffect都是基於這個類實現的,所以他們自然也支援pauseresumestop這三個方法。

最後就是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回撥時傳入了三個引數。分別是newValueoldValueboundCleanup。前兩個引數大家都很熟悉,第三個引數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。

相關文章