1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】Promiseの配列を並列実行/逐次実行するには?

Last updated at Posted at 2024-12-19

Promiseの静的メソッド

プロミスの並列実行には、Promiseクラスの静的メソッドが使えます。
これにはall, allSettled, any, raceがあり、以下の共通点があります。

  • 引数: 反復可能オブジェクトArrayなど)
    • 要素にPromiseがあったら、実行が終わるまで待機する
    • 要素にはプロミス以外のものを入れてもOK
  • 戻り値: 実行したいプロミスに影響されたプロミス

それぞれのメソッドでは、返されたプロミスの履行と拒否のタイミングが違います。
表にまとめるとこのようになります。

メソッド名 履行タイミング 拒否タイミング
all 全てのプロミスが履行されたとき いずれかのプロミスが拒否されたとき
allSettled 全てのプロミスが決定されたとき なし
any いずれかのプロミスが履行されたとき 全てのプロミスが拒否されたとき
race いずれかのプロミスが履行されたとき いずれかのプロミスが拒否されたとき

ここからは各メソッドの解説です。

Promise.all

  • 履行: 全てのプロミスが履行されたとき
  • 拒否: いずれかのプロミスが拒否されたとき

個人的に一番よく使うメソッドです。
どれかのプロミスが拒否されたらすぐに検知できるので、ひとまずこれを使っておけばOKな印象があります。

例えば、以下のように一定時間待つと履行されるプロミスの配列があります。

const promises = [
  new Promise(resolve => setTimeout(() => resolve("First"), 1000)),
  new Promise(resolve => setTimeout(() => resolve("Second"), 2000)),
  new Promise(resolve => setTimeout(() => resolve("Third"), 3000))
];

これを並列実行し、履行された結果の配列を取得できます。
このとき実行時間は、一番時間がかかる最後のプロミスの実行時間となります。

const array = await Promise.all(promises)

// 実行には3000ミリ秒かかる
array // ["First", "Second", "Third"]

詳細はMDNをご覧ください。

Promise.allSettled

  • 履行: 全てのプロミスが決定されたとき
  • 拒否: このプロミスは拒否されない

決定というのはプロミスが履行か拒否されたときです。
つまりallSettLedの戻り値は、与えられた全てのプロミスが履行か拒否されたときに履行されます。

また、allSettledの戻り値のプロミスは、以下のオブジェクトが入った配列で履行されます。

  • status: そのプロミスが履行(fulfilled) or 拒否(rejected)されたか
  • value: 履行されたときの値、statusfulfilledのときのみ存在
  • reason: 拒否された理由、statusrejectedのときのみ存在

例えばこのような感じです。

const promises = [
  new Promise(resolve => setTimeout(() => resolve("First"), 1000)),
  // 2000ms経ったら拒否されるプロミス
  new Promise((_, reject) => setTimeout(() => reject("Second"), 2000)),
  new Promise(resolve => setTimeout(() => resolve("Third"), 3000))
];

const array = await Promise.allSettled(promises)

// arrayの中身:
[
  { "status": "fulfilled", "value": "First" },
  { "status": "rejected", "reason": "Second" }, // rejectedになっている
  { "status": "fulfilled", "value": "Third" }
]

詳細はMDNをご覧ください。

Promise.any

  • 履行: いずれかのプロミスが履行されたとき
  • 拒否: 全てのプロミスが拒否されたとき

anyいずれかのプロミスを履行するためのメソッドと言い換えられると思います。
全てのプロミスが拒否=どのプロミスも履行できなくなって、初めて拒否されるイメージです。

また、anyの戻り値のプロミスは、与えられた中で最初に履行されたプロミスで履行されます。

const promises = [
  new Promise((_, reject) => setTimeout(() => reject("First"), 1000)),
  new Promise(resolve => setTimeout(() => resolve("Second"), 2000)),
  new Promise((_, reject) => setTimeout(() => reject("Third"), 3000))
];

const result = await Promise.any(promises)
// 2000msかかる
result // 'Second'

詳細はMDNをご覧ください。

Promise.race

  • 履行: いずれかのプロミスが履行されたとき
  • 拒否: いずれかのプロミスが拒否されたとき

raceはいずれかのプロミスが決定されたときに決定されます。
これはつまり、どのプロミスが最初に決定されるかを競うレースのようなものと捉えられそうです。

const promises = [
  new Promise((_, reject) => setTimeout(() => reject("First"), 1000)),
  new Promise(resolve => setTimeout(() => resolve("Second"), 2000)),
  new Promise((_, reject) => setTimeout(() => reject("Third"), 3000))
];

await Promise.race(promises)
// エラー: Uncaught First

詳細はMDNをご覧ください。

for await...of

Promiseの静的メソッドではありませんが、似たような用途に使える構文としてfor await...ofが挙げられます。

これはPromiseの静的メソッドと違ってまとめて実行結果を返すのではなく、プロミスの実行結果を使って何かの処理を行えます。

fof...ofに慣れ親しんでいる方からすれば、説明するより実際に見た方がわかりやすいかもしれません。

const promises = [
  new Promise(resolve => setTimeout(() => resolve("First"), 1000)),
  new Promise(resolve=> setTimeout(() => resolve("Second"), 3000)),
  new Promise(resolve => setTimeout(() => resolve("Third"), 2000)),
];

// forの後にawaitをつける
for await (const value of promises) {
  // プロミスの実行結果であるvalueを使った処理
  console.log(value)
}

// 1000ms後: First
// さらに2000ms後: Second
// さらに0ms後: Third

並列になるか逐次になるか

JavaScriptのプロミスは以下の特徴を持っています。

  • プロミスの実行は作成された時点(newされた時点)から開始される
  • 一度作ったプロミスの実行を止める方法は、プロミスからは用意されていない

newされた時点とは言葉通りの意味で、例えばこんな感じになります。

const promises = [
  new Promise(resolve => setTimeout(() => {
    console.log("First")
    resolve()
  }, 1000)),
  new Promise(resolve => setTimeout(() => {
    console.log("Second")
    resolve()
  }, 2000)),
  new Promise(resolve => setTimeout(() => {
    console.log("Third")
    resolve()
  }, 3000)),
];

// 3つのプロミスの実行はすでに始まっている
// 開始から1000ミリ秒後: First
// 開始から2000ミリ秒後: Second
// 開始から3000ミリ秒後: Third

ここでは配列の中に3つのプロミスを作成しています。
プロミスはnewした時点で実行開始されるので、一見ただ配列を作っただけに見えてもsetTimeoutは実行されます。

つまり、同時に作成されたプロミスは並列実行のように振る舞います。

JavaScriptはシングルスレッドなので、実際に行われる処理は1つです。
そのため実際には並列実行されないこともあります。

JavaScript はもともとシングルスレッドなので、異なるプロミス間で制御が移り、プロミスの実行が同時に行われるように見えても、指定された瞬間には 1 つのタスクしか実行されないことに注意してください。JavaScript で並列実行を行うには、ワーカースレッドを使うしかありません。 - MDN

なお、今回使用しているsetTimeoutは、(シングルスレッドでも)複数のタイムアウトを一度に実行できます。
そのためこの影響は受けないと思っていいと思います。

逐次実行するには?

プロミスを逐次実行したければ、プロミスを作成するタイミングを逐次にする必要があります。
一度にまとめて作成するのではなく、必要になったときに順番に作成する感じです。

例えば、1000ms経ったらログに文字を出力する処理を考えます。
この処理は関数として実装し、関数は都度作成したプロミスを返します。
これは以下のような実装でできます。

function run(msg) {
  return new Promise(resolve => {
    console.log(`start ${msg}`)
    setTimeout(() => {
      console.log(`done ${msg}`) // 1000ms経ったらログを出力する
      resolve(msg) // 与えられた文字列で解決する
    }, 1000)
  })
}

逐次実行になる例

一番単純な方法は、for...of文の中にawaitを書くことです。
await run(msg)のように順番にプロミスを作成していけば、簡単に逐次実行できます。

const messages = ['A', 'B', 'C']

for (const msg of messages) {
 // 前のループが終わるまでプロミスを作成しない
  await run(msg)
}

console.log('done all') // 1000 * 3 = 3000ms後に実行される
出力
// 開始直後
start A
// 1000ms後
done A
start B
// さらに1000ms後
done B
start C
// さらに1000ms後
done C
done all

for文の中でawaitを書いた場合、awaitしたプロミスの実行が終わるまで次のループに行きません。
そのためプロミスが順番に作成され、その結果逐次実行になったというわけです。

並列実行になる例

しかし、この関数を以下のように呼び出してしまうと、逐次実行できません。

const messages = ['A', 'B', 'C']

// 同時にプロミスを作成してしまう
const promises = messages.map(msg => run(msg))

for (const promise of promises) {
  await promise // 実行が終わるまで待つ
}

console.log('done all') // 1000 * 3 = 3000ms後に実行されるつもりで書いている
出力
// 開始直後
start A
start B
start C
// 1000ms後
done A
done B
done C
done all

これはmessages.map(msg => run(msg))の時点で3つのプロミスを同時に作成してしまっているからです。
その結果、全てのプロミスの実行が同時に始まってしまいました。

参考: この記事

逐次実行する方法

先ほど紹介したように、普通のループ内でプロミスを作成してawaitすれば逐次実行できます。
それ以外にも、Array.fromAsyncが便利なメソッドとして使えます。

Array.fromAsyncは配列などから新しい配列を作成する静的メソッドです。
この際要素にプロミスが含まれていた場合、それを解決した値が使われます。

  • 引数: 反復可能オブジェクト
  • 戻り値: 新しく作成された配列で履行されるプロミス

これは割とPromise.allに似ています。
後述のマッピング関数を使わない場合、動作はほぼ同じです。

const messages = ['A', 'B', 'C']

// プロミスの入った配列を作成
const promises = messages.map(msg => run(msg))

// この2つはどちらも並列実行される
const result = await Promise.all(promises)
const result = await Array.fromAsync(promises)

Array.fromAsyncが取ることができる引数は反復可能オブジェクトだけではありません。
以下のようなものが使えます。

  • 非同期反復可能オブジェクト
  • 反復可能オブジェクト(配列など) <- 今回使っているやつ
  • Array-Like(配列風)オブジェクト

マッピング関数

Array.fromAsyncで逐次実行する場合、第二引数のmapFnが使えます。
ここには関数が入り、これは配列のそれぞれの要素のマッピングします。

そして、mapFnの戻り値をプロミスにした場合、プロミスの決定を待機(要するにawait)してから次に行きます。
(おそらく)待機が終わるまでプロミスが作成されないので、逐次実行に利用できます。

要するに、逐次実行できるよう、内部でいい感じにプロミスを作成してくれるということです。

例えば、先ほどのArray.fromAsyncを逐次実行で動かすには、以下のように書き換えられます。

const messages = ['A', 'B', 'C']

// messagesを使い、mapFnにプロミスを返す関数を入れる
await Array.fromAsync(messages, msg => run(msg))
出力
// 開始直後
start A
// 1000ms後
done A
start B
// さらに1000ms後
done B
start C
// さらに1000ms後
done C
1
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?