2022年4月13日

Fetch: 中断

ご存知のように、fetch はPromiseを返します。そして、JavaScriptには一般的にPromiseを「中断」する概念がありません。では、実行中の fetch をキャンセルするにはどうすればよいでしょうか?例えば、サイト上のユーザーの操作が、fetch がもはや必要ないことを示している場合などです。

このような目的のために、特別な組み込みオブジェクト AbortController があります。これは fetch だけでなく、他の非同期タスクも中断するために使用できます。

使い方は非常に簡単です

AbortController オブジェクト

コントローラーを作成する

let controller = new AbortController();

コントローラーは非常にシンプルなオブジェクトです。

  • abort() というメソッドを1つ持ち、
  • イベントリスナーを設定できる signal というプロパティを1つ持ちます。

abort() が呼び出されると

  • controller.signal"abort" イベントを発行します。
  • controller.signal.aborted プロパティは true になります。

一般的に、プロセスには2つの当事者がいます

  1. キャンセル可能な操作を実行する側、これは controller.signal にリスナーを設定します。
  2. キャンセルする側、これは必要に応じて controller.abort() を呼び出します。

完全な例を以下に示します(まだ fetch は使用していません)

let controller = new AbortController();
let signal = controller.signal;

// The party that performs a cancelable operation
// gets the "signal" object
// and sets the listener to trigger when controller.abort() is called
signal.addEventListener('abort', () => alert("abort!"));

// The other party, that cancels (at any point later):
controller.abort(); // abort!

// The event triggers and signal.aborted becomes true
alert(signal.aborted); // true

ご覧のとおり、AbortControllerabort() が呼び出されたときに abort イベントを渡すための手段にすぎません。

AbortController オブジェクトを使用せずに、コード内で同じ種類のイベントリスニングを独自に実装することもできます。

しかし、重要なのは、fetchAbortController オブジェクトの使用方法を知っていることです。それは fetch に統合されています。

fetch での使用

fetch をキャンセルできるようにするには、AbortControllersignal プロパティを fetch オプションとして渡します

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});

fetch メソッドは AbortController の使用方法を知っています。 signalabort イベントをリッスンします。

अब, 中断するには, controller.abort() を呼び出します

controller.abort();

これで完了です。fetchsignal からイベントを取得し、リクエストを中断します。

fetch が中断されると、その Promise は AbortError で拒否されるため、例えば try..catch で処理する必要があります。

1 秒後に中断される fetch の完全な例を以下に示します

// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

try {
  let response = await fetch('/article/fetch-abort/demo/hang', {
    signal: controller.signal
  });
} catch(err) {
  if (err.name == 'AbortError') { // handle abort()
    alert("Aborted!");
  } else {
    throw err;
  }
}

AbortController はスケーラブルです

AbortController はスケーラブルです。複数の fetch を一度にキャンセルできます。

多くの urls を並列してフェッチし、単一のコントローラーを使用してすべてを中断するコードのスケッチを以下に示します

let urls = [...]; // a list of urls to fetch in parallel

let controller = new AbortController();

// an array of fetch promises
let fetchJobs = urls.map(url => fetch(url, {
  signal: controller.signal
}));

let results = await Promise.all(fetchJobs);

// if controller.abort() is called from anywhere,
// it aborts all fetches

fetch とは異なる独自の非同期タスクがある場合、単一の AbortController を使用して、fetch とともにそれらを停止できます。

タスク内で abort イベントをリッスンするだけで済みます

let urls = [...];
let controller = new AbortController();

let ourJob = new Promise((resolve, reject) => { // our task
  ...
  controller.signal.addEventListener('abort', reject);
});

let fetchJobs = urls.map(url => fetch(url, { // fetches
  signal: controller.signal
}));

// Wait for fetches and our task in parallel
let results = await Promise.all([...fetchJobs, ourJob]);

// if controller.abort() is called from anywhere,
// it aborts all fetches and ourJob

まとめ

  • AbortController は、abort() メソッドが呼び出されると、その signal プロパティで abort イベントを生成するシンプルなオブジェクトです(また、signal.abortedtrue に設定します)。
  • fetch はそれと統合されています。signal プロパティをオプションとして渡し、fetch はそれをリッスンするため、fetch を中断できます。
  • コードで AbortController を使用できます。「abort() を呼び出す」→「abort イベントをリッスンする」という相互作用はシンプルで普遍的です。 fetch なしでも使用できます。
チュートリアルマップ