牛逼!在Vue3.5中僅僅2分鐘就能封裝一個自動cancel的fetch函式

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

前言

在歐陽的上一篇 這應該是全網最詳細的Vue3.5版本解讀文章中有不少同學對Vue3.5新增的onWatcherCleanup有點疑惑,這個新增的API好像和watch API回撥的第三個引數onCleanup功能好像重複了。今天這篇文章來講講新增的onWatcherCleanup函式的使用場景:封裝一個自動cancel的fetch函式

關注公眾號:【前端歐陽】,給自己一個進階vue的機會

watch回撥的第三個引數onCleanup

有些同學可能還不清楚watch回撥的第三個引數onCleanup,我們先來看個demo,程式碼如下:

watch(id, (value, oldValue, onCleanup) => {
  console.log("do something");
  onCleanup(() => {
    console.log("cleanup");
  });
});

watch回撥的前兩個引數大家應該很熟悉,分別是value新的值,oldValue舊的值。

第三個引數onCleanup大家平時可能用的不多,這是一個回撥函式,當watch的值改變後或者元件銷燬前就會執行onCleanup傳入的回撥。

在上面的demo中就是變數id改變時會觸發onCleanup中的回撥,進而console列印"cleanup"字串。又或者所在的元件銷燬前也會觸發onCleanup中的回撥,進而console列印"cleanup"字串。

那我們在onCleanup中可以幹嘛呢?

答案是可以清理副作用,比如在watch中使用setInterval初始化一個定時器。那麼我們就可以在onCleanup的回撥中清理掉定時器,無需去元件的beforeUnmount鉤子函式去統一清理。

onWatcherCleanup函式

onWatcherCleanup函式的作用和watch回撥的第三個引數onCleanup差不多,也是當watch的值改變後或者元件銷燬前就會執行onWatcherCleanup傳入的回撥。

使用方法也很簡單,程式碼如下:

import { watch, onWatcherCleanup } from "vue";

watch(id, () => {
  console.log("do something");
  onWatcherCleanup(() => {
    console.log("cleanup");
  });
});

從上面的程式碼可以看到onWatcherCleanup的用法其實和watch回撥的第三個引數onCleanup差不多,區別在於這裡的onWatcherCleanup是從vue中import匯入的。

除了從vue中import匯入的區別以外,還有一個區別是onWatcherCleanup不光在watch中可以使用,在watchEffect中同樣也可以使用。比如下面這樣的:

watchEffect(() => {
  console.log("do something in watchEffect", id.value);
  onWatcherCleanup(() => {
    console.log("cleanup watchEffect");
  });
});

和前面的例子一樣,上面的程式碼中id的值改變後或者元件銷燬時也會執行onWatcherCleanup函式中的console.log列印。

onWatcherCleanup函式是從vue中import匯入的,那麼這意味著onWatcherCleanup函式的呼叫可以寫在任意地方,只要最終經過函式的層層呼叫後還是在watch或者watchEffect的回撥中就可以。

利用上面的這一特點我們可以使用onWatcherCleanup做到一些onCleanup做不到的事情,比如:封裝一個自動cancelfetch函式。

封裝自動cancel的fetch函式

在講這個之前我們先來了解一下如何cancel一個fetch函式。

這裡涉及到AbortController介面,AbortController 介面表示一個控制器物件,允許你根據需要中止一個或多個 Web 請求。

下面這個是cancel取消一個請求的demo,程式碼如下:

const controller = new AbortController();
const res = await fetch(url, {
  ...options,
  signal: controller.signal,
});

setTimeout(() => {
  controller.abort();
}, 500);

首先使用new AbortController()建立一個控制器物件controller

其中的controller.signal返回一個 AbortSignal 物件例項,可以用它來和非同步操作進行通訊或者中止這個操作。

在我們這裡把controller.signal作為signal選項直接傳給fetch函式就可以了。

最後就是可以使用controller.abort()將fetch請求取消掉,在上面的demo中是如果超過500ms請求還沒完成,那麼就執行controller.abort()將fetch請求取消掉。

有了前面的知識鋪墊,我們先來看看使用“自動cancelfetch函式”的地方,程式碼如下:

<script setup lang="ts">
import { watch, ref, watchEffect, onWatcherCleanup } from "vue";
import myFetch from "./myFetch";

const id = ref(1);
const data = ref(null);

watch(id, async () => {
  const res = await myFetch(`http://localhost:3000/api/${id.value}`, {
    method: "GET",
  });
  console.log(res);
  data.value = res;
});
</script>

<template>
  <p>data is: {{ data }}</p>
  <button @click="id++">id++</button>
</template>

在上面的例子中使用watch監聽了變數id,在監聽的回撥中會使用封裝的myFetch函式請求介面。

上面的例子大家平時應該經常遇到,如果id的值變化很快,但是服務端介面請求需要2秒才能完成,這時我們期望只有最後一次id的值改變觸發的請求才需要完成,其他請求都cancel取消掉。

如果在myFetch請求的過程中元件被銷燬了,此時我們也期望能夠將請求cancel取消掉。

在Vue3.5之前想要去實現上面的這兩個需求很麻煩,但是有了Vue3.5的onWatcherCleanup函式後就非常容易了。

這個是封裝的自動cancelfetch函式,myFetch.ts檔案程式碼如下:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  const controller = new AbortController();
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }

  const res = await fetch(url, {
    ...options,
    signal: controller.signal,
  });

  let json;
  try {
    json = await res.json();
  } catch (error) {
    json = {
      code: 500,
      message: "JSON format error",
    };
  }
  return json;
}

由於onWatcherCleanup函式是從vue中import匯入,那麼我們就可以在自己封裝的myFetch函式中匯入和使用他。

onWatcherCleanup函式的回撥中我們執行了controller.abort(),前面已經講過了當watch或者watchEffect的回撥執行前或者元件解除安裝前就會執行裡面的onWatcherCleanup註冊的回撥。我們這裡的myFetch是在watch中呼叫的,當然也會觸發裡面的onWatcherCleanup註冊的回撥。

onWatcherCleanup的回撥中執行了controller.abort(),前面我們講過了執行controller.abort()就會將正在請求的fetch函式給cancel取消掉。

就這麼簡單的就實現了前面的兩個需求:

需求一:如果id的值變化很快,但是服務端介面請求需要2秒才能完成,這時我們期望只有最後一次id的值改變觸發的請求才需要完成,其他請求都cancel取消掉。下面這個是變數id在短時間內多次修改的gif效果圖:
click

從上面的gif圖可以看到只有最後一個請求是完成了的,其他請求全部被cancel掉。

需求二:如果在myFetch請求的過程中元件被銷燬了,此時我們也期望能夠將請求cancel取消掉。下面這個是元件解除安裝時gif效果圖:
hide

從上圖中可以看到在解除安裝元件時元件正在從服務端請求資料,此時請求會自動cancel掉。

細心的小夥伴發現了在myFetch函式中,onWatcherCleanup函式外面套了一個getCurrentWatcher的判斷,程式碼如下:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  // ...省略
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }
  // ...省略
}

當watch或者watchEffect監聽的值改變後onWatcherCleanup的回撥就會觸發,所以onWatcherCleanup的執行是由其所在的watch或者watchEffect觸發的。

如果onWatcherCleanup不在watch或者watchEffect的回撥中執行,那麼當然onWatcherCleanup中的回撥也永遠不會執行。

可能有的小夥伴有疑問,你這裡的onWatcherCleanup是在myFetch中執行的,也沒在watch或者watchEffect的回撥中執行吖?

答案是myFetch函式的執行是在watch中執行的,myFetch然後再去執行onWatcherCleanup

getCurrentWatcher()函式就會返回當前正在執行回撥的watch或者watchEffect,如果當前myFetch不是在watch或者watchEffect的回撥中執行的,那麼getCurrentWatcher()函式的返回值就是空,所以這種情況就不需要去執行onWatcherCleanup函式了。

最後值得一提的是onWatcherCleanup不能在await後面執行,比如下面這樣的程式碼:

import { getCurrentWatcher, onWatcherCleanup } from "vue";

export default async function myFetch(url: string, options: RequestInit) {
  const controller = new AbortController();
  const res = await fetch(url, {
    ...options,
    signal: controller.signal,
  });

  let json;
  try {
    json = await res.json();
  } catch (error) {
    json = {
      code: 500,
      message: "JSON format error",
    };
  }
  // ❌ 錯誤的寫法
  if (getCurrentWatcher()) {
    onWatcherCleanup(() => {
      controller.abort();
    });
  }

  return json;
}

在上面的程式碼中我們將onWatcherCleanup呼叫放在了await fetch()的後面,這種寫法onWatcherCleanup註冊的回撥是不會執行的

為什麼在await後面的onWatcherCleanup註冊的回撥永遠不會執行呢?

答案是js的await相當於註冊了一個回撥函式去執行await後的程式碼,當await等待結束後再去執行這個回撥函式,從而執行await後的程式碼。

await以及之前的程式碼確實是在watch回撥中執行的,我們這裡的onWatcherCleanup就是await後面的程式碼,await後面的程式碼是在一個新的回撥中執行的,也就是watch“回撥中”的“回撥中”執行的。

onWatcherCleanup執行時已經不知道當前正在執行的watch回撥是誰了,所以onWatcherCleanup的回撥也沒註冊上。當watch的變數修改時或者元件解除安裝時onWatcherCleanup註冊的回撥永遠也不會執行。

總結

watch或者watchEffect監聽的變數修改時,以及元件解除安裝時,會去執行他們回撥中使用onWatcherCleanup註冊的回撥函式。並且onWatcherCleanup是從vue中import匯入的,使得我的可以在任意地方執行onWatcherCleanup函式。利用這兩個特性我們就可以封裝一個自動cancel的fetch函式。

關注公眾號:【前端歐陽】,給自己一個進階vue的機會

另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。

相關文章