JavaScript 課程 – Fetch 與網路請求(Networking)
主題:AbortController(取消請求)
簡介
在前端開發中,fetch 已成為與伺服器溝通的主要手段。它的非同步特性讓 UI 能保持流暢,但同時也帶來一個問題:當使用者在請求尚未完成時切換頁面、點擊其他按鈕或是條件改變,我們往往需要主動取消這些尚在進行的請求,以避免不必要的流量、競爭條件(race condition)或是錯誤訊息干擾。
AbortController 正是為了解決這類需求而設計的 API。它提供了一個 「取消信號」(AbortSignal),讓開發者可以在任何時刻通知 fetch(或其他支援 AbortSignal 的 API)立即中止請求。掌握這個工具,不僅能提升使用者體驗,還能減少資源浪費與潛在的程式錯誤。
本篇文章將從概念、語法、實作範例到最佳實踐,完整說明如何在 JavaScript 中使用 AbortController 來管理與取消網路請求,適合 初學者到中階開發者 閱讀與實作。
核心概念
1. AbortController 與 AbortSignal
AbortController:一個控制器,用來產生與管理 取消訊號。它本身不會直接中止請求,而是提供一個signal屬性(AbortSignal)給其他 API 使用。AbortSignal:一個只讀的物件,代表「是否已被取消」的狀態。當AbortController.abort()被呼叫時,signal.aborted會變成true,同時會觸發signal上的abort事件。
// 建立一個 AbortController
const controller = new AbortController();
// 取得對應的 AbortSignal
const signal = controller.signal;
// 監聽 abort 事件(可選)
signal.addEventListener('abort', () => {
console.log('請求已被取消');
});
2. 在 fetch 中使用 AbortSignal
fetch 的第二個參數(init 物件)接受 signal 屬性。只要把 controller.signal 傳入,fetch 就會在 controller.abort() 被呼叫時自動中止。
fetch(url, { signal })
.then(response => {
// 正常處理回應
})
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch 被取消');
} else {
console.error('其他錯誤', err);
}
});
3. 為何需要手動取消?
| 情境 | 若不取消會發生什麼? |
|---|---|
| 使用者快速切換搜尋關鍵字 | 先前的請求仍會回傳,可能覆蓋最新結果,導致 UI 顯示過時資料 |
| 表單提交重複點擊 | 多個相同請求同時發送,浪費頻寬與伺服器資源 |
| 長時間的下載/上傳 | 使用者離開頁面或取消操作,仍會持續佔用網路與記憶體 |
| 需要超時機制 | fetch 本身沒有內建 timeout,必須自行結合 AbortController 實作 |
程式碼範例
以下示範 3~5 個實用範例,涵蓋基本取消、搜尋防抖、超時、串流取消與多請求協調。
範例 1️⃣:基本取消請求
// 取得資料的函式
function loadData(url) {
const controller = new AbortController();
const signal = controller.signal;
// 5 秒後自動取消(示範用)
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(url, { signal })
.then(res => {
clearTimeout(timeoutId); // 成功回應就清除 timeout
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
})
.then(data => console.log('取得資料:', data))
.catch(err => {
if (err.name === 'AbortError') {
console.warn('請求被使用者或 timeout 取消');
} else {
console.error('其他錯誤:', err);
}
});
// 回傳 controller,讓外部可以自行 abort
return controller;
}
// 使用
const ctrl = loadData('https://api.example.com/items');
// 例如使用者點了「取消」按鈕
document.getElementById('cancelBtn').addEventListener('click', () => ctrl.abort());
重點:
controller.abort()只需要呼叫一次,即可讓所有綁定的fetch立即中止。
範例 2️⃣:搜尋防抖(Debounce)+ 取消前一次請求
let lastController = null;
function search(query) {
// 若前一次請求仍在執行,先取消
if (lastController) lastController.abort();
const controller = new AbortController();
lastController = controller;
fetch(`https://api.example.com/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then(res => res.json())
.then(results => {
// 只在最新的請求成功時更新 UI
renderResults(results);
})
.catch(err => {
if (err.name === 'AbortError') return; // 前一次請求被取消,無需顯示錯誤
console.error(err);
});
}
// 防抖函式(簡易版)
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 綁定輸入框
const input = document.getElementById('searchBox');
input.addEventListener('input', debounce(e => search(e.target.value), 300));
技巧:把
controller存在變數中,讓每次呼叫search前先取消前一次的請求,避免競爭條件。
範例 3️⃣:實作 fetch 超時(Timeout)機制
function fetchWithTimeout(url, options = {}, timeout = 8000) {
const controller = new AbortController();
const { signal } = controller;
const timer = setTimeout(() => controller.abort(), timeout);
// 合併使用者傳入的 signal(如果有的話)
const combinedSignal = signal;
const fetchOptions = { ...options, signal: combinedSignal };
return fetch(url, fetchOptions)
.finally(() => clearTimeout(timer)) // 不管成功或失敗,都清除計時器
.catch(err => {
if (err.name === 'AbortError') {
throw new Error(`請求逾時(超過 ${timeout}ms)`);
}
throw err;
});
}
// 使用
fetchWithTimeout('https://api.example.com/slow-endpoint', {}, 3000)
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err.message));
說明:
AbortController可與setTimeout結合,實作出 自訂的 timeout,因為原生fetch沒有內建 timeout 功能。
範例 4️⃣:取消可讀取的流(ReadableStream)
async function streamLargeFile(url) {
const controller = new AbortController();
const signal = controller.signal;
// 讓使用者可以透過按鈕取消下載
document.getElementById('stopBtn').addEventListener('click', () => controller.abort());
try {
const response = await fetch(url, { signal });
if (!response.body) throw new Error('此瀏覽器不支援 streaming');
const reader = response.body.getReader();
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
received += value.length;
console.log(`已接收 ${received} 位元組`);
}
console.log('下載完成');
} catch (err) {
if (err.name === 'AbortError') {
console.warn('下載被使用者取消');
} else {
console.error('下載錯誤', err);
}
}
}
// 執行
streamLargeFile('https://example.com/large-video.mp4');
重點:即使在 stream 讀取階段,只要
controller.abort()被呼叫,reader.read()會拋出AbortError,讓程式能即時停止讀取。
範例 5️⃣:多個請求共享同一個 AbortSignal(一次取消多個請求)
function fetchMultiple(urls) {
const controller = new AbortController();
const signal = controller.signal;
const promises = urls.map(u =>
fetch(u, { signal })
.then(r => r.json())
.catch(err => {
if (err.name === 'AbortError') {
console.warn(`URL ${u} 被取消`);
} else {
console.error(`URL ${u} 錯誤`, err);
}
})
);
// 10 秒後全部取消
setTimeout(() => controller.abort(), 10000);
return Promise.allSettled(promises);
}
// 呼叫
fetchMultiple([
'https://api.example.com/a',
'https://api.example.com/b',
'https://api.example.com/c',
]).then(results => console.log(results));
應用:在需要同時發送多筆請求(例如平行載入多個資源)時,只要一次
abort()即可同時終止所有請求,省去逐一管理的麻煩。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記傳入 signal |
建立 AbortController 後未把 signal 加到 fetch,導致 abort() 沒有效果。 |
確認 fetch(url, { signal }) 中的 signal 正確傳遞。 |
多次呼叫 abort() |
重複呼叫不會產生錯誤,但可能在錯誤處理上產生多餘的訊息。 | 在呼叫前檢查 controller.signal.aborted,或只在需要時呼叫一次。 |
| 與其他庫衝突 | 某些第三方 HTTP 客戶端(如 Axios)在舊版中不支援 AbortSignal。 |
使用支援 AbortController 的版本,或自行在庫中加入 signal 參數。 |
忘記清除 setTimeout |
若使用 timeout,未在成功回應時清除計時器,會在之後不必要觸發 abort()。 |
在 then 或 finally 中 clearTimeout(timer)。 |
| 在舊版瀏覽器無法使用 | IE、舊版 Safari 不支援 AbortController。 |
透過 polyfill(如 abortcontroller-polyfill)或回退到 XMLHttpRequest。 |
| 錯誤判斷 | 只捕捉 err,未檢查 err.name === 'AbortError',導致取消請求被誤當成失敗。 |
在 catch 中先判斷 err.name 再決定處理方式。 |
最佳實踐:
- 一個請求一個 Controller:除非需要一次取消多個請求,否則每個
fetch建議使用獨立的AbortController,避免不小心取消到其他請求。 - 在 UI 元件生命周期內管理:例如在 React 的
useEffect清除函式、Vue 的beforeUnmount中呼叫abort(),確保元件卸載時不會留下懸掛的請求。 - 結合防抖 / 防重複提交:如搜尋框或表單送出,使用
AbortController搭配防抖(debounce)或節流(throttle)可以大幅降低不必要的網路流量。 - 自訂 Timeout:將
AbortController包裝成fetchWithTimeout,在專案中統一使用,提升可維護性。 - 記得清理資源:
clearTimeout、移除事件監聽器(signal.removeEventListener)都是防止記憶體洩漏的好習慣。
實際應用場景
| 場景 | 為什麼需要 AbortController | 典型實作 |
|---|---|---|
| 即時搜尋(Autocomplete) | 使用者快速輸入,舊的搜尋請求若完成會覆寫最新結果。 | 防抖 + 取消前一次請求(範例 2) |
| 大檔案下載/串流 | 使用者可能在下載途中點「取消」或離開頁面。 | 取消 ReadableStream(範例 4) |
| 表單重複送出 | 防止使用者因網路慢而多次點擊「送出」按鈕。 | 按鈕點擊時先 abort() 前一次請求 |
| API 超時保護 | 某些 API 回應時間不穩定,需在限定時間內失敗。 | fetchWithTimeout(範例 3) |
| 多資源平行載入 | 首頁需要一次載入多張圖片、JSON、CSS 等。若使用者離開頁面,全部請求都應立即停止。 | 共享同一 AbortSignal(範例 5) |
| 單頁應用(SPA)路由切換 | 路由變換時舊頁面的資料請求仍在進行,可能導致記憶體佔用或錯誤 UI 更新。 | 在路由守衛或 useEffect 清除函式中 abort() |
總結
AbortController 為 JavaScript 非同步網路請求 提供了簡潔且強大的取消機制。透過以下步驟,你可以輕鬆將它納入日常開發:
- 建立
AbortController,取得signal。 - 將
signal傳入fetch(或其他支援的 API)。 - 在適當時機呼叫
controller.abort()(使用者操作、超時、元件卸載等)。 - 在
catch中辨識AbortError,避免將取消當成真正錯誤顯示。
配合 防抖、節流、timeout 等技巧,AbortController 能有效降低不必要的流量、避免 UI 被過時資料污染,並提升使用者體驗。
在現代前端框架(React、Vue、Angular)中,將它與生命週期或 Hook 結合,更能確保資源在元件銷毀時即時釋放,避免記憶體洩漏。
掌握這項工具,你的前端程式碼將會更 健壯、可維護且具備彈性,在面對日益複雜的網路互動需求時,亦能從容應對。祝開發順利,玩得開心! 🎉