配列のPromiseを直列で実行する方法

2 min read読了の目安(約2400字 1

始めに

配列のPromiseを並列で実行する場合はPromise.allを使うと思いますが、直列で順番に実行するにはどうするか少し悩みました。パッと調べたときにあんまりいい情報が見つからなかったのでまとめてみました。

並列実行
const waitTimes = [300, 100, 200, 500, 400];

function wait(waitTime: number) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${waitTime}ms waited.`);
      resolve();
    }, waitTime);
  });
}

(async () => {
  // Promise.allで並列に実行される
  await Promise.all(
    waitTimes.map(async (waitTime) => {
      await wait(waitTime);
    })
  );
})();

Promiseを直列で実行する方法

最も単純な方法(forループ)

身も蓋もない話ですが、forループ内でawaitを書くで順番に実行することが出来ます。
ただ最近mapfilterなど配列操作系のメソッドを使い続けていてあまりforループは使いたくなかったので、別な方法も考えました。

単純な方法で直列に実行する
(async () => {
  for (let i = 0; i < waitTimes.length; i++) {
    await wait(waitTimes[i]);
  }
})();

ちなみにforEachで書くのはNGです。関数を渡してしまうため、async-awaitの構文が更に一つ中入ってしまうためです。各項目内は待ってくれますが、全体の処理としては影響を受けません。並列実行に近い挙動をしますが、Promise.allと違って全ての実行が終わったかの確認も行われないためたちが悪いです。

// 全部の項目が先に実行されてしまう
waitTimes.forEach(async (waitTime) => {
  // 関数内部のawaitは適用される
  await wait(waitTime);
});

配列操作系のメソッドでの実装(reduceによる畳み込み)

Promiseは以下のように.then()をつないでいくことで順番に実行していくことができます。

Promise.resolve()
  .then(() => wait(100))
  .then(() => wait(200));

これを上手く配列操作系のメソッドでやる場合はreduceを使用します。第2引数にPromise.resolve()を渡してresolveされたPromiseを初期値として与え、毎回promise.then()を実行して返すことで繋げることができます。

reduceによる直列実行
(async () => {
  await waitTimes.reduce((promise, waitTime) => {
    return promise.then(async () => {
      await wait(waitTime);
    });
  }, Promise.resolve());
})();

終わりに

以上がPromiseを直列で実行する方法でした。個人的には配列操作系のメソッドを使った方が全ての要素に同じ処理をする意図がハッキリしているのでこちらの方を使いたいのですが、今回のケースだと結構複雑になって悩ましいなと思いました。forループの方が圧倒的に分かりやすいんですよね。。カッコつけたい人は是非reduceの方を使ってみてください(笑)
今回のサンプルコードは以下で確認できるので、興味があるかたは見てください。

https://www.typescriptlang.org/play?#code/MYewdgzgLgBA7gQwJZQCpILYFMIwLwwDaAzAAykA0MAjOVQEx0wCsTALOQLoDcAULwDMArmGBQk4eMigAKRCnTYAXDDBCMAIywAnAJQwA3rxgxtWKEO1hVWODAAK2kBiQQsAHgBuIJABMAfDIyZhAgADaeWPp4-obGJjBuaJhYIEKyMtGxRgkJoJDhWAB0YSAA5jIABgAkBvLJ2AC+GLj1WL5Flbp8uSYh4ZGZPQmNVPWKUcON3byN-DIIEACeojCZ+NnxAPRbMIBkcoDoSoD52oAyEfH5oWHFpRUA5AAOCNoIYVdhMAhiEpBFtzMmCHqDicLjcRReYRk8RM4xSECKGAQ9wWy1WcmkEyycV6HyB9XRChS-xGuniM22u0AL27HM4mC6FErlGS3NzaJAvD5fcDwv7DAQgbRrK6wJD4GCkbgwUXuKSE7Dwq5gMpQAAWkqQAGoNfocglAdJZbJYfLCEhOMS5uduQybszWez3p9xNz6L9ifqUIaJvCzL4hMAsEF7iDXFgxhiiRtsQkzBYrDBg85Q0VVVgwCiVsA1ljdbkPbB8cbJtCYNMplRHEmwf0IoHdDNpkN4kA