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
: 履行されたときの値、status
がfulfilled
のときのみ存在 -
reason
: 拒否された理由、status
がrejected
のときのみ存在
例えばこのような感じです。
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