本文 AI 產出,尚未審核

Vue3 非同步與資料請求 – Cancel Request 機制


簡介

在單頁應用 (SPA) 中,前端常會同時發出多筆 HTTP 請求,例如即時搜尋、分頁資料載入或定時自動刷新。若使用者在請求尚未完成前就切換頁面、改變條件或觸發新請求,舊的請求仍會在背景執行,可能造成 資源浪費、畫面顯示錯誤,甚至產生 記憶體洩漏。

Vue3 提供了完整的生命週期與 Composition API,讓我們可以在適當時機 取消 (cancel) 未完成的請求,保證 UI 與資料狀態的一致性。本篇文章將說明取消請求的核心概念、實作方式,以及在 Vue3 中的最佳實踐,幫助你寫出更健全、效能更佳的前端程式碼。


核心概念

1. 為什麼需要取消請求?

情境 可能的問題
使用者在搜尋框快速輸入文字,觸發多筆 API 呼叫 前一次的回傳結果可能在後一次之後渲染,導致 顯示過時的資料。
切換路由或離開當前元件時仍有未完成的請求 請求完成後仍會嘗試更新已銷毀的元件,產生 Vue 警告 ([Vue warn]: Cannot read property 'xxx' of undefined)。
長時間的檔案上傳或大量資料下載 若使用者中途取消操作,仍會持續佔用 頻寬與伺服器資源。

結論:適時取消請求是提升使用者體驗與降低資源消耗的關鍵。

2. 兩大取消機制:AbortController 與 Axios Cancel Token

2.1 AbortController(原生 Fetch API)

AbortController 是瀏覽器原生提供的 API,配合 fetch 或任何支援 AbortSignal 的函式庫使用。它的使用方式非常簡潔:

const controller = new AbortController();   // 建立控制器
fetch(url, { signal: controller.signal })   // 把 signal 傳入 fetch
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('請求已被取消');
    } else {
      console.error(err);
    }
  });

// 需要取消時呼叫
controller.abort();
  • 優點:不需額外套件、支援所有支援 AbortSignal 的函式庫(如 axios@1.0+、vue-query)。
  • 缺點:舊版瀏覽器(IE)不支援,需要 polyfill。

2.2 Axios Cancel Token(舊版)

在 axios@0.22 之前,取消請求的方式是使用 Cancel Token:

import axios from 'axios';

const source = axios.CancelToken.source();

axios.get('/api/users', {
  cancelToken: source.token
}).then(res => console.log(res))
  .catch(thrown => {
    if (axios.isCancel(thrown)) {
      console.log('請求被取消:', thrown.message);
    } else {
      console.error(thrown);
    }
  });

// 取消請求
source.cancel('手動取消');

提醒:自 axios@1.0 起已改為使用 AbortController,建議直接改用原生方式。

3. 在 Vue3 中結合生命週期與 Composition API

Vue3 的 onUnmounted、watchEffect、watch 等 API 能讓我們在元件銷毀或條件變化時自動執行取消動作。

3.1 基本範例:在 setup 中使用 AbortController

<script setup>
import { ref, onUnmounted } from 'vue';

const data = ref(null);
const error = ref(null);
let controller = null; // 讓外部可以存取

function loadData() {
  // 若先前有未完成的請求,先取消
  if (controller) controller.abort();

  controller = new AbortController();
  fetch('/api/articles', { signal: controller.signal })
    .then(res => res.json())
    .then(json => (data.value = json))
    .catch(err => {
      if (err.name !== 'AbortError') error.value = err;
    });
}

// 初始載入
loadData();

// 元件卸載時自動取消
onUnmounted(() => {
  if (controller) controller.abort();
});
</script>

3.2 與 watch 結合:條件變更即取消舊請求

<script setup>
import { ref, watch } from 'vue';

const query = ref('');          // 由搜尋框綁定
const result = ref([]);
let abortCtrl = null;

watch(query, (newVal, oldVal) => {
  // 每次 query 改變都重新發送請求,先取消前一次
  if (abortCtrl) abortCtrl.abort();

  abortCtrl = new AbortController();
  fetch(`/api/search?q=${encodeURIComponent(newVal)}`, {
    signal: abortCtrl.signal,
  })
    .then(r => r.json())
    .then(json => (result.value = json))
    .catch(e => {
      if (e.name !== 'AbortError') console.error(e);
    });
});
</script>

3.3 建立可重用的 Composable:useCancelableFetch

// src/composables/useCancelableFetch.js
import { ref, onUnmounted } from 'vue';

/**
 * 取得資料的可取消 fetch
 * @param {string} url 請求的 URL
 * @param {object} options fetch 的選項 (可省略)
 * @returns {{ data: Ref, error: Ref, loading: Ref, cancel: Function }}
 */
export function useCancelableFetch(url, options = {}) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);
  let controller = null;

  const fetchData = async () => {
    // 若已有執行中的請求,先取消
    if (controller) controller.abort();

    controller = new AbortController();
    loading.value = true;
    try {
      const res = await fetch(url, { ...options, signal: controller.signal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      data.value = await res.json();
    } catch (e) {
      // 只在非取消錯誤時設定 error
      if (e.name !== 'AbortError') error.value = e;
    } finally {
      loading.value = false;
    }
  };

  // 立即執行一次
  fetchData();

  // 元件卸載自動取消
  onUnmounted(() => {
    if (controller) controller.abort();
  });

  return { data, error, loading, cancel: () => controller?.abort(), refetch: fetchData };
}

使用方式:

<script setup>
import { useCancelableFetch } from '@/composables/useCancelableFetch';

const { data, error, loading, cancel, refetch } = useCancelableFetch('/api/dashboard');
</script>

3.4 路由變更時自動取消(Vue Router 4)

<script setup>
import { onBeforeRouteLeave } from 'vue-router';
import { ref } from 'vue';

const controller = new AbortController();
const items = ref([]);

fetch('/api/items', { signal: controller.signal })
  .then(r => r.json())
  .then(json => (items.value = json))
  .catch(e => {
    if (e.name !== 'AbortError') console.error(e);
  });

// 當離開當前路由時自動取消
onBeforeRouteLeave((to, from, next) => {
  controller.abort(); // 立即取消
  next();
});
</script>

3.5 與 axios 結合(axios@1+ 使用 AbortController)

import axios from 'axios';
import { ref, onUnmounted } from 'vue';

const data = ref(null);
let abortCtrl = null;

function load() {
  if (abortCtrl) abortCtrl.abort();
  abortCtrl = new AbortController();

  axios
    .get('/api/profile', { signal: abortCtrl.signal })
    .then(res => (data.value = res.data))
    .catch(err => {
      if (err.name !== 'AbortError') console.error(err);
    });
}

// 呼叫一次
load();

// 清理
onUnmounted(() => abortCtrl?.abort());

常見陷阱與最佳實踐

陷阱 說明 解決方案
忘記在 catch 中判斷 AbortError 直接把錯誤顯示給使用者,會把「已取消」的訊息當成錯誤。 在 catch 內 if (err.name !== 'AbortError') 再處理。
重複使用同一個 AbortController 先前請求仍在執行,會被新請求意外終止。 每次請求都建立新的 AbortController,或使用可重用的 composable。
在 watchEffect 中直接返回 Promise 會產生「未處理的 rejected promise」警告。 使用 async 函式或在 watchEffect 內自行捕獲錯誤。
取消後未清除參考 controller 仍保留在記憶體中,長時間使用會累積。 在 onUnmounted 或取消後把變數設為 null。
忘記在路由離開時取消 離開頁面後仍有請求回傳,導致 UI 更新錯位。 使用 onBeforeRouteLeave 或 onUnmounted 取消。

最佳實踐清單

  1. 每筆請求配一個 AbortController,不要在多個請求間共用同一個實例。
  2. 在 catch 中先檢查 AbortError,只對真正的錯誤做 UI 提示。
  3. 將取消邏輯封裝成 composable(如 useCancelableFetch),讓所有元件共享同一套取消策略。
  4. 在元件銷毀或路由離開時呼叫 abort(),確保不會有「幽靈請求」影響其他頁面。
  5. 配合防抖 (debounce) 或節流 (throttle) 使用,減少不必要的請求次數,從根本降低取消需求。

實際應用場景

1. 即時搜尋 (Typeahead)

使用者每輸入一個字元即發送搜尋請求,舊的請求若未完成就會被新請求取代。透過 watch + AbortController,可以保證只有最新的搜尋結果會渲染。

2. 分頁或無限捲動

切換頁碼或向下滾動時會同時發出多筆資料請求。若使用者快速跳頁,先前的請求應立即取消,避免「舊頁資料」覆寫「新頁資料」。

3. 後台儀表板自動刷新

儀表板每 30 秒自動呼叫 API 取得最新指標。若使用者在刷新間隔內切換至其他頁面,應自動取消當前的輪詢請求,以免不必要的網路流量。

4. 大檔案上傳/下載

使用 XMLHttpRequest 或 fetch 上傳大檔時,提供「取消」按鈕讓使用者自行中止,並在 UI 上即時回饋中止狀態。

5. 多語系或主題切換

切換語系或主題時會重新載入對應的 JSON 配置檔。舊的載入任務若仍在執行,可能導致 UI 閃爍或錯誤顯示,使用取消機制即可避免。


總結

在 Vue3 的單頁應用中,取消未完成的 HTTP 請求不僅能提升使用者體驗,還能減少不必要的網路與記憶體資源消耗。透過原生的 AbortController 或新版 Axios 的同等支援,我們可以在:

  • 搜尋框、分頁、無限捲動 等高頻互動場景即時取消舊請求;
  • 路由離開、元件銷毀 時自動清理,避免幽靈請求;
  • 可重用 composable 把取消邏輯抽象化,提升專案維護性。

記得遵守 每次請求一個 controller、捕獲 AbortError、在適當生命週期取消 的最佳實踐,你的 Vue3 應用將會更加穩定、效能更佳。祝開發順利,玩得開心! 🎉