非同期処理の扱いが容易になる(と僕は思う)Promise。では、何個もPromiseを実行したいとき ― 例としていろいろなAPIを一気に呼び出したいとき ― はどうすればよいのでしょうか。
順序を問わずに呼び出す
1. 単純に並べる
fetch("https://httpstat.us/200").then(result => result.text()).then(text => console.log(text));
fetch("https://httpstat.us/201").then(result => result.text()).then(text => console.log(text));
fetch("https://httpstat.us/202").then(result => result.text()).then(text => console.log(text));
fetch("https://httpstat.us/203").then(result => result.text()).then(text => console.log(text));
愚直に並べます。が、こうするくらいなら配列で回したくないですか?
2. forEach
const targets = [
"https://httpstat.us/200",
"https://httpstat.us/201",
"https://httpstat.us/202",
"https://httpstat.us/203"
];
targets.forEach(target => {
fetch(target).then(result => result.text()).then(text => console.log(text));
});
処理の内容がスッキリしました。
正直ここまではPromise関係ないので、Promiseな話題に戻します。
処理は並列、結果はいつも順番に
前節では、リクエストはしっかり並列して行われています。しかし、返り値(ログの出力)の順番が一定ではありません。ここからは、順番を意識したリクエストの投げ方を失敗例を交えて紹介していきます。
1. 単純に並べる
await fetch("https://httpstat.us/200").then(result => result.text()).then(text => console.log(text));
await fetch("https://httpstat.us/201").then(result => result.text()).then(text => console.log(text));
await fetch("https://httpstat.us/202").then(result => result.text()).then(text => console.log(text));
await fetch("https://httpstat.us/203").then(result => result.text()).then(text => console.log(text));
単純にawait
で並べました。前のリクエストを待ってから次のリクエストが発生するので、当然実行に時間がかかります。では、リクエストを並列しつつ表示は順番にするにはどうすれば良いのでしょうか。
2. 失敗例: array.forEach
const targets = [
"https://httpstat.us/200",
"https://httpstat.us/201",
"https://httpstat.us/202",
"https://httpstat.us/203"
];
targets.forEach(async target => {
await fetch(target).then(result => result.text()).then(text => console.log(text));
});
forEachのコールバック関数をasyncにするパターンです。が、この処理はうまく実行されません。
forEachのコールバック関数の中ではawaitされますが、コールバック関数の実行そのものはawaitされないためです。
2-1. for文
const targets = [
"https://httpstat.us/200",
"https://httpstat.us/201",
"https://httpstat.us/202",
"https://httpstat.us/203"
];
for(let target of targets) {
await fetch(target).then(result => result.text()).then(text => console.log(text));
};
forEach
が使えないのであれば純粋なfor-of
文に戻してあげよう、という発想です。しかし、これでも処理が直列に行われてしまっています。非同期を同期してどうする!ということで続いては、
3. array.map
const targets = [
"https://httpstat.us/200",
"https://httpstat.us/201",
"https://httpstat.us/202",
"https://httpstat.us/203"
];
targets.map(async target => {
await fetch(target).then(result => result.text()).then(text => console.log(text));
});
配列の内容をもとに新たな配列を作るmapメソッドです。この時点ではforEachと結果は変わりません。が、作られた配列を見てみると、
Promiseの配列ができています。これがヒント。「async関数は実行されるとPromiseを返す」ので、その返り値のPromiseが配列になった形です。
ということは、このPromise群を後に一括で処理してあげればよいですね。
4. Promise.all
const targets = [
"https://httpstat.us/200",
"https://httpstat.us/201",
"https://httpstat.us/202",
"https://httpstat.us/203"
];
Promise.all(
targets.map(target => fetch(target).then(result => result.text()))
).then(results => results.forEach(text => console.log(text)));
これで処理は並列・結果は順番になります。嬉しい!!!
さて、ここで新キャラ、Promise.all
の登場です。少しずつ噛み砕いていきましょう。
targets.map(target => fetch(target).then(result => result.text()))
URLの配列をマップし、fetch( ).then( )
の返り値であるPromiseの配列にします。
await Promise.all( ... ).then(results => results.forEach(text => console.log(text)));
Promise.all
もまたPromiseを返します。Promise.all
は、引数として渡された配列内のすべてのPromiseがfulfilledになる(または1つでもrejectされる)のを待って、その結果をresolve / rejectします。ですから、
results === ["200 OK", "201 Created", "202 Accepted", "203 Non-Authoritative Information"]
となります。あとは、この結果を順に表示するだけ。簡単!!!
ちなみに、Promise.all
の中のPromiseの実行順序は保証されていません(並列実行される)が、返り値の配列は呼び出し順と必ず同一になるようになっています。
Promise.all([promise1, promise2, promise3, ...])
.then(results => {
// 実際の完了順に関わらずresultsの中身は常に[promise1のresult, promise2のresult, promise3のresult, ...]
});
Promise.allもまたPromiseを返すということは、awaitも使えるので、
(await Promise.all( ... ))
.forEach(text => console.log(text));
としても同じ結果が得られます。こちらのほうがネストが深くならず読みやすいかも...?