async/awaitをつかった非同期処理の直列実行

最近、非同期処理を直列に実行するプログラムを書く機会が多くあったのでメモ。

この間、自分が書いたのは、以下の様なもの。

  • ユーザーは複数のデータを選択できる
  • ユーザーが確定ボタンを押したら、選択した最初のデータについて、Web APIを呼び出して、そのレスポンスの内容によってはダイアログを表示する
  • ユーザーがダイアログを閉じたら(これはダイアログを開くAPIにコールバックを渡すことでハンドリングできる)、次のデータについて上と同じ処理をおこなう。全てのデータにこれをおこなう

非同期処理を直列に実行する場合のイディオムとして知られているのが、「Promiseを返す」関数を返すクロージャを用意して、そのクロージャの配列でreduce操作をおこなうもの。コードにするとこんなかんじ。

const genWait = (n) => {
  return () => {
    return new Promise((resolve) => {
      console.log(`Waiting ${n}ms...`);
      setTimeout(() => {
        resolve(n);
      }, n);
    });
  };
};

const genPromiseFns = [
  genWait(3000),
  genWait(1000),
  genWait(2000)
];

const done = genPromiseFns.reduce((previous, fn) => {
  return previous.then((n) => {
    return fn();
  });
}, Promise.resolve(0))

done.then((n) => {
  console.log('done all');
});

reduceのコールバックの第一引数に先の非同期処理のPromise(最初はreduceの第二引数)が渡されるので、その解決を待って、第二引数のクロージャを呼び出す。doneで配列の末尾の要素のクロージャの返したPromiseを参照できるので、それが解決されれば、全ての非同期処理が終わったことになる。

これはイディオムとしてよく知られたものではあるのだけど、知らない人からすれば非同期処理を直列に実行しているように理解しづらいのと、reduce周辺のコードを書くのがややこしい(個人の感想)。 ECMAScriptの仕様の策定途上にあるasync/await(Async Functions)を使えば、非同期処理があたかも同期処理のように書ける。

const genWait = (n) => {
  return () => {
    return new Promise((resolve) => {
      console.log(`Waiting ${n}ms...`);
      setTimeout(() => {
        resolve(n);
      }, n);
    });
  };
};

const genPromiseFns = [
  genWait(3000),
  genWait(1000),
  genWait(2000)
];

(async () => {
  for (let fn of genPromiseFns) {
    await fn();
  }
  console.log('done all');
})();

策定プロセスのstage3(仕様は完成している状態)だから、Babelのpreset(babel-preset-stage-3)を使って、ぼちぼち実戦で使おうかな、と思いつつ、普段書くPromiseを扱う処理がそんなに複雑じゃなかったので(せいぜい2つか3つかの固定の数のPromiseをPromise.allで待ち受ける程度)、スルーしていた。モッタイナイ。