JavaScript | 非同期処理:async / await – await とループ

JavaScript JavaScript
スポンサーリンク

await とループを一言でいうと

await をループの中で使うときのポイントは、
「そのループを“順番にゆっくり回す”のか、“できるだけ同時に走らせる”のかを意識すること です。

同じループでも、

  • 各要素を「1 個ずつ順番に await する書き方」
  • 全要素の処理を「まとめて並列に走らせてから await する書き方」

があり、これを意識せず書くと、
「無駄に遅いコード」や「思ったとおり動かないコード」になりがちです。

ここが重要です。
await は「その行で一旦止まる」ので、
ループの中に置けば「ループを止める」ことにもなります。
止めたいのか、止めたくないのかを、自分で決めて書けるようになることが大切です。


await と相性が良いループ:for / for…of

1件ずつ順番に処理したいときの基本形

まず、「配列を順番に処理したい」典型パターンから。

async function processItemsSerial(items) {
  for (const item of items) {
    const result = await doAsync(item);
    console.log("処理結果:", result);
  }
  console.log("全部終わり");
}
JavaScript

このコードの動きはこうです。

  1. 最初の要素で doAsync(item) を実行
  2. await でその結果を待つ(終わるまで次のループには進まない)
  3. 結果をログに出す
  4. 次の要素に進む
  5. これを配列の最後まで繰り返す

つまり、「1 件ずつ順番に処理する」直列処理 です。

順番に処理することに意味があるケース

例えば、

  • API に「1秒に1回まで」という制限があるとき
  • 「前の結果が次の処理に影響する」ようなとき
  • 「順番どおりに処理していきたい」仕様のとき

などは、あえてこの「直列」のループが必要になります。

async function uploadInOrder(files) {
  for (const file of files) {
    const result = await uploadFile(file);
    console.log("アップロード完了:", result);
  }
}
JavaScript

ここが重要です。
for / for…of の中で await を使うと、「配列の長さ × 非同期処理時間」のぶんだけ、きっちり時間がかかる。
それは“遅い”のではなく、“順番を守る”という意味で正しい挙動です。
“順番が必要かどうか”を自分で判断することが大事です。


よくある落とし穴:forEach と await

forEach の中で await は「待ってくれない」

初心者がよくハマるポイントがこれです。

async function processItems(items) {
  items.forEach(async (item) => {
    const result = await doAsync(item);
    console.log("結果:", result);
  });
  console.log("forEach の外側");
}
JavaScript

パッと見、「順番に await してくれそう」に見えますが、
実際にはこうなります。

  • forEach はコールバックを「同期的に」全部呼び出してしまう
  • そのコールバック内の await は、その関数の中で止まるだけ
  • processItems 関数自体は、forEach が終わった瞬間に次へ進む

そのため、「forEach の外側」のログは、
doAsync が終わる前に先に実行されてしまいます。

つまり、forEach
「ループの外から見て await を効かせたい」用途には向いていません。

なぜ for / for…of をすすめるのか

for / for…of なら、
ループ自体が await によって“止まる”ので、
「全部終わってから次に進む」という直感どおりの動きになります。

async function processItems(items) {
  for (const item of items) {
    const result = await doAsync(item);
    console.log("結果:", result);
  }
  console.log("全部終わったあとにこれが出る");
}
JavaScript

ここが重要です。
「ループ全体として、await をちゃんと効かせたい」なら、
forEachmap のコールバックに async を渡すのではなく、
素直に for / for...of で書いたほうが、挙動もコードも分かりやすくなります。


配列に対する「並列処理」:map + Promise.all

全要素を同時に処理したい場合

今度は逆に、こういう状況を考えます。

  • 各アイテムの処理は「互いに独立」
  • 1 件ずつ順番にやる必要はない
  • できるだけ早く全部終わらせたい

こういうときは、「配列を map して Promise の配列を作り、Promise.all でまとめて await」するのが定番です。

async function processItemsParallel(items) {
  const promises = items.map((item) => {
    return doAsync(item);  // doAsync は Promise を返す
  });

  const results = await Promise.all(promises);

  console.log("全部の結果:", results);
}
JavaScript

動きはこうです。

  1. items.map(...) が、全アイテムに対して doAsync(item) を一気に呼び出す
    → 各処理が“ほぼ同時に”スタート
  2. promises は、「各処理の Promise が入った配列」になる
  3. Promise.all(promises) で、「全部終わるまで待つ Promise」を作る
  4. await でその完了を待ち、results に全結果が配列で入る

もし doAsync が 1 件あたり 1 秒かかる処理でも、
配列が 10 件だとして、
直列なら約 10 秒、並列なら約 1 秒ちょっとで終わるイメージです。

map + Promise.all のイディオム

まとめると、
「配列を全部並列で処理して、結果を全部欲しい」パターンは以下の形を覚えてしまって構いません。

const results = await Promise.all(items.map(item => doAsync(item)));
JavaScript

ここが重要です。
「順番に処理する for + await」と
「並列で処理する map + Promise.all + await」を、
意識的に使い分けられるかどうかが、
“await とループを理解できているか” の一つの分かれ目です。


直列 vs 並列:どちらを選ぶかの判断基準

直列 for + await が向いているケース

例えば次のような条件なら、「順番に await」するべきです。

  • 「前の処理が終わらないと、次の処理の内容が決められない」
    例:userId を取り、その userId で投稿を取り、その投稿 ID でコメントを取る
  • API の利用制限を守りたい(リクエストを間引きたい)
  • ファイルを 1 件ずつ順番に上書きしたいなど、順序性が重要な処理

並列 map + Promise.all が向いているケース

こちらは「順番にやる意味はない」「できるだけ早く終わってほしい」ケース。

  • 商品 ID のリストに対して、在庫情報をそれぞれ取得
  • ユーザーの一覧に対して、プロフィール画像をそれぞれダウンロード
  • いくつかの外部 API を同時に叩いて、全部揃ったら画面を描画

要は、「全部終われば良い。誰が先でも構わない」 場面です。

ここが重要です。
ループの中で await を見つけたら、
「これは本当に直列にする必要があるか?」
「個々が独立しているなら、Promise.all にできないか?」
と一度問い直す習慣を持つと、
非同期処理のパフォーマンスと設計力が一気に上がります。


ちょっと応用:for…of で“制限付き並列”をする考え方

「全部いっぺんに」はヤバいこともある

Promise.all で一気に並列にすると、
例えば 1000 件のリクエストを同時に飛ばしてしまう可能性があります。

それは、

  • サーバー側に負荷をかけすぎる
  • ブラウザや Node.js のコネクション数上限に引っかかる

などの問題につながります。

「いくつかずつ並列に処理する」発想

ここは少し応用になりますが、
for…of と await で、「同時に走らせる数を制限する」という書き方もできます。

例えば「最大 3 件まで並列にしたい」など。

初学者の段階でここまで厳密に書けなくても構いませんが、
「直列か全部並列か」の二択ではなく、
「並列度合いを調整する」という考え方がある、ということだけ頭の片隅に置いておくとよいです。

ここが重要です。
await とループは、「完全直列」と「完全並列」だけではなく、
“どのくらい並列にするか” を調整する道具にもなりえます。
今はざっくりでいいので、「並列度」という観点があると知っておいてください。


初心者として「await とループ」で本当に押さえてほしいこと

for / for…of の中で await を使うと、「1 件ずつ順番に処理する」直列ループになる。
これは「遅い」のではなく、「順序を守っている」と理解する。

forEach のコールバックで async / await を使っても、
ループ全体としては待ってくれない。
「ループの外側からちゃんと待ちたい」ときは、for / for…of を使う。

「配列の要素を全部“同時に”処理して結果だけ欲しい」ときは、
await Promise.all(items.map(item => doAsync(item))) という形が定番。

直列ループにするか並列処理にするかは、
「前の結果に依存しているか」「順番が意味を持つか」を基準に判断する。

ここが重要です。
await とループを使いこなす鍵は、「時間」と「依存関係」を意識することです。
“本当にここで止まりたいのか?”
“ここはみんなで一斉に走り出してもいい場所じゃないか?”
と自分に問いかけながら await の場所を決めていくと、
非同期処理が“ただ動くだけ”から“自分で設計した動き”に変わっていきます。

最後に、小さな練習を置いておきます。

// 1. 1秒後に値を返す doAsync(i) を作る(i をそのまま返してもよい)。
// 2. 配列 [1,2,3,4,5] を「直列 for...of + await」で処理して、
//    毎回のログと全体の時間を測ってみる。
// 3. 同じ処理を「map + Promise.all + await」で書いて、
//    ログと時間の違いを比べてみる。
JavaScript

自分の目で「どこで止まっているか」「どれだけ待ち時間が変わるか」を確かめると、
await とループの関係が、かなり立体的に見えてくるはずです。

タイトルとURLをコピーしました