AbortControllerとは?
AbortController
はJavaScriptに組み込まれているAPIの一つで、何かを中断する処理の実装手段です。
特に非同期処理を中断するのに便利ですが、非同期処理以外でも使うことができます。
AbortController
を使うには、中断したい処理(関数 / メソッド)がAbortController
に対応している必要があります。
簡単に言うと、関数がAbortSignal
を受け取れるなら対応してると思います。
使い方
AbortController
オブジェクトはコンストラクタで作成できます。
const controller = new AbortController()
controller // AbortController
このcontroller
は以下のプロパティ / メソッドを持っています。
-
abort
: 処理を中断する -
signal
:abort
で中断する処理を決める、AbortSignal
のインスタンス
この記事では、AbortController
のインスタンスのことをコントローラー、AbortSignal
のインスタンスのことをシグナルと呼びます。
例えば以下のsampleAPI
関数は、オプションとしてsignal
を受け取ります。
この関数の処理は以下の手順で停止できます。
- オプションの
signal
にコントローラーのsignal
プロパティ(AbortSignal
)を入れる - 同じコントローラーの
abort
メソッドを呼び出す
コードはこのようになります。
// あらかじめコントローラーを作成する
const controller = new AbortController()
// sampleAPIを呼び出す
sampleAPI({ signal: controller.signal })
controller.abort() // 中断される
このcontroller.abort
を呼び出すと、sampleAPI
関数は内部で自動的に処理を中断してくれます。
fetch
関数の例
fetch
関数はHTTPリクエストを送る関数です。
そして、まさに上のsampleAPI
関数のような引数を取ります。
この関数は以下の引数を取ります。
- リクエストしたいURL(文字列)
- オプションが入るオブジェクト
-
signal
:AbortSignal
のインスタンス - それ以外にもいろいろなオプションがある
-
AbortController
を使わない場合の呼び出しは単純です。
fetch
はプロミスを返すのでawait
し、第一引数にURLを入れます。
const response = await fetch('https://example.com')
resposne.header // Header { ... }
AbortController
を使う
HTTPリクエストにはそれなりに時間やコストがかかります。
そのため何らかの理由でそのリクエストが不要になったら、早めに中断したいです。
そして、その中断する方法がAbortController
です。
以下のようなコードで実行を中断できます。
const controller = new AbortController()
await fetch('https://example.com', { signal: controller.signal })
controller.abort() // リクエストを中断する
// キャンセルされたことを示す例外が発生する
fetch
関数はキャンセルされた時に、AbortError
という名前のDOMException
例外を発生させます。
そのため実際に使う際は、try-catch
などでエラーを補足する必要があります。
関数を自作する
AbortSignal
を受け取ることで、処理を中断できるようにする関数を自作することもできます。
AbortSignal
とは
APIを作る上で欠かせないのがAbortSignal
です。
AbortSignal
は以下の特徴を持っています。
-
EventTarget
を継承している - ↑なので、DOMでおなじみの
addEventListener
が使える - コントローラーで
abort
を呼び出すとabort
イベントが配信される
コントローラーで
abort
を呼び出すとabort
イベントが配信される
このため、どのように処理をキャンセルするか、もっと言うと abort
イベントが配信されたときどんな処理をするかがキャンセルの実態になります。
function sampleAPI({ signal }) { // AbortSignalを受け取る
// メイン処理
// abortイベントをリッスンする
signal.addEventListener('abort', () => {
// ここでメイン処理をどう中断するかが重要
})
}
詳細はMDNにもあるので、併せてご覧ください。
もしAbortSignal
に対応しているAPIをラップするだけなら、シグナルをそのまま渡せばOKです。
これはfetch
のラップなどが含まれます。
async function wrap({ signal }) {
// fetchにシグナルを渡す
return await fetch('https://example.com', { signal })
}
作例: タイマー
ここでは、一定時間処理を待機するsleep
関数を実装します。
この関数は以下の引数を取ります。
-
ms
: 待つ時間、ミリ秒 -
{ signal }
: 処理を中断するためのAbortSignal
- ここでは一定時間待機するのを中断する
また、sleep
関数は一定時間待ったら履行されるプロミスを返します。
このプロミスは処理が中断されても履行されます。
実装は例えばこのようになります。
function sleep(ms, { signal } = {}) {
return new Promise(resolve => {
const id = setTimeout(resolve, ms)
// abortされたらタイムアウトをキャンセル
signal?.addEventListener('abort', () => {
clearTimeout(id)
resolve() // プロミスは履行する
})
})
}
使用例:
// コントローラーを準備
const controller = new AbortController()
// 3000ms経ったらログを出力
sleep(3000, { signal: controller.signal })
.then(() => console.log('完了'))
// 300秒経ったらタイマーをキャンセル
await sleep(300) // このタイマーはキャンセルされない
controller.abort()
// 3000ms経たずに「完了」が出力される
実装方法
まず、キャンセル処理がないsleep
を実装してみます。
これはsetTimeout
を使い、一定時間経ったらresolve
が呼び出されるようになっています。
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
// 使用例
await timer(300)
console.log('300ms経過') // 300ms後に出力される
そして、ここにキャンセル処理を書き足していきます。
実装方法は以下の通りです。
-
setTimeout
にはキャンセル方法としてclearTimeout
がある - プロミス内で
abort
イベントをリッスンし、そこでclearTimeout
を呼ぶ
clearTimeout
clearTimeout
は引数にタイムアウトのIDを取ります。
このIDはsetTimeout
関数が戻り値として返すので、これを保管する必要があります。
// ms経ったらfuncを実行する
const id = setTimeout(func, ms)
// タイマーをキャンセルする
clearTimeout(id) // funcは実行されなくなる
abort
イベントをリッスンする
シグナルのabort
イベントをリッスンし、配信されたら上述のclearTimeout
を実行します。
また、プロミスは履行しておきます。
signal?.addEventListener('abort', () => {
clearTimeout(id)
resolve() // プロミスは履行する
})
ここではオプショナルチェーン(?.
)を使って、signal
があるときのみaddEventListener
を実行しています。
そして、ここまでの処理を組み合わせると、先ほどのような実装になります。
function sleep(ms, { signal } = {}) {
return new Promise(resolve => {
const id = setTimeout(resolve, ms)
// abortされたらタイムアウトをキャンセル
signal?.addEventListener('abort', () => {
clearTimeout(id)
resolve() // プロミスは履行する
})
})
}
ここではsetTimeout
の戻り値であるIDをid
に入れ、abort
イベントが配信されたらclearTimeout(id)
を実行するようにしています。
これによって、呼び出し側からsleep
の実行を中断できるようになっています。
中断後の動作は好きにカスタマイズできます。
ここではプロミスを履行し、その後のコードが即座に実行されるようにしていますが、反対にプロミスを拒否することも可能です。
return new Promise((resolve, reject) => {
// ...
signal?.addEventListener('abort', () => {
clearTimeout(id)
reject(new Error('cancel')) // プロミスを拒否する
})
}