非同期処理の中断
- 「中断」の概念 -> AbortSignalを非同期処理のスコープに注入する
参考
AbortSignal
- DOM仕様の一部. しかし, Node.js, Deno, CloudflareWorkersといったJSエンジン(V8)実行環境でも利用できる.
- 非同期処理(関数)の引数オプションとして注入して, AbortSignalを通じて非同期ルーチンを中断させる.
- 様々なBrowserAPI(WebAPI)がAbortSignalに対応している.
AbortController
- AbortSignalオブジェクトをプロパティに持つ管理オブジェクト
- *.signalでAbortSignalを参照できる
独自定義の非同期処理への注入
// AbortControllerの定義
const controller = new AbortController();
// 非同期処理にAbortSignalを注入
(async function({signal: controller.signal}) {
// 非同期ルーチン
...
if (signal) {
return;
}
})();
// 非同期ルーチンに中断通知
controller.abort(); // Notify
fetchAPIへの注入
// AbortControllerの定義
const controller = new AbortController();
fetch("https://example.com", { signal: controller.signal })
.then((data) => {
...
})
.catch(err => {
...
})
.finally(() => {
...
});
// 非同期ルーチンに中断通知
controller.abort(); // Notify
addEventListenerへの注入
// AbortControllerの定義
const controller = new AbortController();
window.addEventListener("resize", () => {
console.log("resize");
}, { signal: controller.signal });
// 非同期ルーチンに中断通知
controller.abort(); // Notify
中止による失敗の取り扱い
- AbortSignalによって非同期処理が中止された場合, 結果は失敗.
- DOMExceptionオブジェクトがスローされる
- err.nameが
AbortErrorとなっている
// AbortControllerの定義
const controller = new AbortController();
// ウィンドウ全体に関するイベント
// loadイベント: *.htmlページが読み込み完了時に処理を発生するイベント
// DOMContentLoadedイベント: DOM要素の解析が完了したタイミングで発生するイベント
// scrollイベント: ウィンドウスクロール時に発生するイベント
// selectstartイベント: テキストが選択されたときに発生するイベント
// resizeイベント: ウィンドウのサイズが変更されらときに発生するイベント
// visibilitychangeイベント: ブラウザのタブの表示状態が変わったときに発生するイベント
// クリックに関するイベント
// clickイベント: クリックしたときに発生するイベント
// dbclickイベント: ダブルクリックしたときに発生するイベント
// マウスイベント
// mousedownイベント
// mouseupイベント
// mouseenterイベント
// mouseleaveイベント
// mouseoverイベント
// mouseoutイベント
// mousemoveイベント
// フォーム操作に関するイベント
// submitイベント: フォームを送信したときに発生するイベント
// resetイベント: フォームをリセットしたときに発生するイベント
// selectイベント: 入力フォームで入力した文字を選択したときに発生するイベント
// inputイベント: フォーム入力に値を入力したときに発生するイベント
// キーボード操作に関するイベント
// keypressイベント: キーを入力したときに発生するイベント
// keydownイベント: キーを押したときに発生するイベント
// keyupイベント: キーを入力してして離したタイミングで発生するイベント
// フォーカス操作に関するイベント
// focusイベント: フォーカス時に発生するイベント
// blurイベント: フォーカスから外れたときに発生するイベント
// changeイベント: オブジェクト値の変更後フォーカスを離したときに発生するイベント
// ドラッグ&ドロップに関するイベント
// dragstartイベント: ドラッグ開始時に発生するイベント
// dragendイベント: ドラッグ終了時に発生するイベント
// dragoverイベント: ドラッグオブジェクト上にある時に発生するイベント
// dragenterイベント: ドラッグ中にオブジェクトに入るときに発生するイベント
// dragleaveイベント: ドラッグ中にオブジェクトから外れるときに発生するイベント
// dragイベント: マウスがドラッグしている間に発生するイベント
// dropイベント: ドロップするときに発生するイベント
// タッチ操作に関するイベント
// touchstartイベント: オブジェクトにタッチしたときに発生するイベント
// touchmoveイベント: オブジェクトをタッチしているときに発生するイベント
// touchendイベント: オブジェクトのタッチが終わるときに発生するイベント
window.addEventListener("DOMContenLoaded", () => {
try {
console.log('DOMの解析終了');
} catch (err => {
if (signal.name ==== 'AbortError') {
console.log(`DOMException: ${signal.name}`);
}
}
}, { signal: controller.signal });
// 非同期ルーチンに中断通知
controller.abort(); // Notify
中止中に失敗を判断する方法
const controller = new AbortController();
fetch('url', { signal: controller.signal })
.then((res) => { console.log("成功", res); })
.catch((err) => {
if (err instanceof DOMException && err.name === "AbortError") {
console.log('中止 by AbortSignal');
} else {
console.log('その他の失敗', err);
}
});
// 非同期ルーチンに中断通知
controller.abort();
中止機能の実装(内部でAbortSignal対応の処理を呼ぶ場合)
const controller: ArbortController = new AbortController();
async function transferMoney(signal: ArbortSignal) {
// 振り込み
await fetch("https://atm/api/withdraw", {
method: 'POST',
mode: 'cors',
credentials: 'include',
redirect: 'omit',
body: JSON.stringify({ user: 'Alice', amount: 1000 }),
signal: signal
});
// 引き出し
await fetch("https://atm/api/deposit", {
method: 'GET',
mode: 'cors',
credentials: 'include',
redirect: 'omit',
body: JSON.stringify({ user: 'Bob', amount: 700 }),
signal: signal
});
// 通知
await fetch("https://atm/api/notification", {
method: "POST",
mode: 'cors',
credentials: 'include',
redirect: 'omit',
body: JSON.stringify({ text: "Alice sent 1000 yen to Bob" }),
signal: signal
});
} // func
- fetch関数の中断処理は, 引数にsignalを入れるだけで良い.
- 中断された時点でPromiseが出力されて, async関数外にロールバックされようとして, catchで捕まえる.
手動で中止処理を行う必要がある場合の実装
- 内部処理がAbortSignalに対応しない場合
- e.g. Web Workerによるワーカースレッドへの委譲 (WebWorkerはpostMessage()関数でしか通信できない)
手動による基本制御
- 非同期処理開始時にWorkerに処理開始命令を送り, 処理が終了したらWorkerから起動元にデータを送る(通知)する処理を実装
e.g.
async function doSomeWorker(signal: AbortSignal) {
...
// AbortSignalが既にシグナル状態の場合, 非同期スコープを抜ける
if (signal.aborted) {
throw signal.reason;
}
// 同等の組み込みメソッド : signal.throwIfAborted()
...
const worker = new Worker('worker.js');
worker.postMessage({ command: 'doSomeWorker' });
// AbortSignalが発動(中止命令)されたらWorkerに中止命令を送る
signal.addEventListner('abort', () => {
worker.postMessage({ command: 'abort' });
});
// Workerの処理が終わるまで待つ
const result = await new Promise((resolve, reject) => {
// 正常終了
worker.addEventListner('message', (event) => {
resolve(event.data);
});
// 異常終了
worker.addEventListner('error', (event) => {
if (event.error === 'aborted') {
reject(signal.reason);
} else {
reject(event.error);
}
});
});
// Promise
return result;
} // doSomeWorker
signal.reasonは, AbortController側で指定できる.
- 具体的には,
controller.abort("理由")で設定する. デフォルトは, AbortError
SignalController以外でのAbortSignalオブジェクトの作り方
中止済みAbortSignalの生成
const signal = AbortSignal.abort("理由");
一定時間後に中止するAbortSignalの生成
const signal = AbortSignal.timeout(1000); // 1秒後
AbortSignalの合成
- 複数のAbortSignalを作り, 合成することで各条件のうちどれか(OR)で
abortする.
async function transferMoney(signal: AbortSignal) {
// 合成
const combinedSignal = AbortSignal.any([
signal,
AbortSignal.timeout(5000)
]);
await fetch('/api/withdraw', {
method: 'POST',
body: JSON.stringify({
user: "Alice",
amount: 1000
}),
signal: combinedSignal
});
}