本文 AI 產出,尚未審核

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() thenfinallyclearTimeout(timer)
在舊版瀏覽器無法使用 IE、舊版 Safari 不支援 AbortController 透過 polyfill(如 abortcontroller-polyfill)或回退到 XMLHttpRequest
錯誤判斷 只捕捉 err,未檢查 err.name === 'AbortError',導致取消請求被誤當成失敗。 catch 中先判斷 err.name 再決定處理方式。

最佳實踐

  1. 一個請求一個 Controller:除非需要一次取消多個請求,否則每個 fetch 建議使用獨立的 AbortController,避免不小心取消到其他請求。
  2. 在 UI 元件生命周期內管理:例如在 React 的 useEffect 清除函式、Vue 的 beforeUnmount 中呼叫 abort(),確保元件卸載時不會留下懸掛的請求。
  3. 結合防抖 / 防重複提交:如搜尋框或表單送出,使用 AbortController 搭配防抖(debounce)或節流(throttle)可以大幅降低不必要的網路流量。
  4. 自訂 Timeout:將 AbortController 包裝成 fetchWithTimeout,在專案中統一使用,提升可維護性。
  5. 記得清理資源clearTimeout、移除事件監聽器(signal.removeEventListener)都是防止記憶體洩漏的好習慣。

實際應用場景

場景 為什麼需要 AbortController 典型實作
即時搜尋(Autocomplete) 使用者快速輸入,舊的搜尋請求若完成會覆寫最新結果。 防抖 + 取消前一次請求(範例 2)
大檔案下載/串流 使用者可能在下載途中點「取消」或離開頁面。 取消 ReadableStream(範例 4)
表單重複送出 防止使用者因網路慢而多次點擊「送出」按鈕。 按鈕點擊時先 abort() 前一次請求
API 超時保護 某些 API 回應時間不穩定,需在限定時間內失敗。 fetchWithTimeout(範例 3)
多資源平行載入 首頁需要一次載入多張圖片、JSON、CSS 等。若使用者離開頁面,全部請求都應立即停止。 共享同一 AbortSignal(範例 5)
單頁應用(SPA)路由切換 路由變換時舊頁面的資料請求仍在進行,可能導致記憶體佔用或錯誤 UI 更新。 在路由守衛或 useEffect 清除函式中 abort()

總結

AbortControllerJavaScript 非同步網路請求 提供了簡潔且強大的取消機制。透過以下步驟,你可以輕鬆將它納入日常開發:

  1. 建立 AbortController,取得 signal
  2. signal 傳入 fetch(或其他支援的 API)
  3. 在適當時機呼叫 controller.abort()(使用者操作、超時、元件卸載等)。
  4. catch 中辨識 AbortError,避免將取消當成真正錯誤顯示。

配合 防抖、節流、timeout 等技巧,AbortController 能有效降低不必要的流量、避免 UI 被過時資料污染,並提升使用者體驗。
在現代前端框架(React、Vue、Angular)中,將它與生命週期或 Hook 結合,更能確保資源在元件銷毀時即時釋放,避免記憶體洩漏。

掌握這項工具,你的前端程式碼將會更 健壯、可維護且具備彈性,在面對日益複雜的網路互動需求時,亦能從容應對。祝開發順利,玩得開心! 🎉