Wiki_JS_非同期処理の中断 - inoueshinichi/Wiki_Web GitHub Wiki

非同期処理の中断

  • 「中断」の概念 -> 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
  });
}