本文 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 的 onUnmountedwatchEffectwatch 等 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 建立可重用的 ComposableuseCancelableFetch

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

最佳實踐清單

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

實際應用場景

1. 即時搜尋 (Typeahead)

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

2. 分頁或無限捲動

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

3. 後台儀表板自動刷新

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

4. 大檔案上傳/下載

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

5. 多語系或主題切換

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


總結

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

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

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