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 取消。 |
最佳實踐清單
- 每筆請求配一個
AbortController,不要在多個請求間共用同一個實例。 - 在
catch中先檢查AbortError,只對真正的錯誤做 UI 提示。 - 將取消邏輯封裝成 composable(如
useCancelableFetch),讓所有元件共享同一套取消策略。 - 在元件銷毀或路由離開時呼叫
abort(),確保不會有「幽靈請求」影響其他頁面。 - 配合防抖 (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 應用將會更加穩定、效能更佳。祝開發順利,玩得開心! 🎉