JavaScript | 非同期処理:非同期の基礎概念 – イベントループ

JavaScript JavaScript
スポンサーリンク

まずイベントループを一言でイメージする

イベントループは、
「JavaScript に“次にやるべき仕事”をひたすら渡し続ける司会進行役」
です。

JavaScript はシングルスレッドで、同時に動ける処理は 1 つだけですが、
setTimeoutfetch、クリックイベント、Promise など、
「あとで実行される処理」がたくさん存在します。

イベントループは、
「今の仕事が終わった? じゃあ次はこれね」
と、タスクキューやマイクロタスクキューから順番に仕事を持ってきて、
JavaScript のメインスレッドを途切れさせずに回し続けます。

ここが重要です。
イベントループは「非同期処理の裏ボス」です。
これを理解すると、「なぜこの順番で実行されるの?」が、ほとんど全部説明できるようになります。


出演者の整理:何と何の間を“ループ”しているのか

4つの主役をもう一度整理する

イベントループの話をする前に、関係者を整理します。

ひとつ目に、コールスタック。
「今どの関数の中にいて、誰から呼ばれているか」の積み重ねメモ。
ここに乗っているものが「今まさに実行中の処理」です。

ふたつ目に、Web API。
setTimeoutfetch、DOM イベントなど、「待つ」「外の世界とやりとりする」担当。
時間を測ったり、ネットワーク通信したり、ユーザーのクリックを監視したりしています。

みっつ目に、タスクキュー。
setTimeout や DOM イベントのコールバックが、「あとで実行してもらうために並んでいる行列」です。

よっつ目に、マイクロタスクキュー。
Promise.thenasync/await の続きなど、「優先度の高い“あとで処理”」が並んでいる特別レーンです。

そしてイベントループは、
これらの間をぐるぐる回って、

「スタックが空いたか?」
「キューに何か溜まっているか?」

をずっと見張りながら、「次の処理」をスタックに乗せ続けます。

イベントループがしていること(超ざっくり)

ざっくり言うと、イベントループは次のように動いています。

「今の実行(スタック)の処理が全部終わった?」
→ はい → じゃあマイクロタスクキューに何かある? あれば全部実行する
→ それも終わった? → はい → じゃあタスクキューの先頭を 1 つ取って実行する
→ またスタックが空くまで待つ
→ 以下ループ……

このサイクルを、ブラウザがひたすら繰り返しています。


具体例1:setTimeout とイベントループの関係

コードと結果を先に見る

console.log("A");

setTimeout(() => {
  console.log("B: timeout");
}, 0);

console.log("C");
JavaScript

この結果は必ず、

A
C
B: timeout

の順になります。「0ミリ秒なのに、なぜ B が最後?」をイベントループで説明します。

裏側での流れをステップごとに追う

まず、「グローバルコード」(このファイルの先頭から)の実行がスタックに乗ります。

console.log("A") がスタックの一番上で実行され、”A” が出ます。
次に setTimeout が呼ばれますが、ここでやっているのは「0ms 後にこの関数を実行して」と Web API に依頼するだけです。
setTimeout 自体の呼び出しが終わると、スタックからは消えます。

console.log("C") を実行し、”C” が出ます。
グローバルコードの実行が最後まで到達すると、スタックは空になります。

一方その頃、Web API 側のタイマーはほぼすぐに「0ms 経過」を検知し、
「このコールバックをタスクキューに入れておいて」とイベントループに渡しています。

イベントループは、スタックが空になったのを確認すると、
タスクキューの先頭から 1つ取り出し、そのコールバックをスタックに積んで実行します。
ここで "B: timeout" が出力される、という流れです。

ここが重要です。
setTimeout(..., 0) は「今すぐ実行」ではなく、
「今のタスク(グローバルコード)が全部終わったあと、“次のタスク”としてキューに入る」
という意味だと理解してください。


具体例2:Promise とイベントループ(マイクロタスクが入るパターン)

コードと結果

console.log("A");

setTimeout(() => {
  console.log("B: timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("C: promise");
});

console.log("D");
JavaScript

ほとんどの環境で、結果はこうなります。

A
D
C: promise
B: timeout

なぜこうなるのか、イベントループの観点から追ってみます。

イベントループ+2種類のキューで見る

“A” のログはそのまま同期で出ます。

setTimeout は先ほどと同じく、Web API に仕事を渡し、
終わったタイミングでコールバックを「通常のタスクキュー」に入れます。

Promise.resolve().then(...) は、
すぐに解決され、その then のコールバックが「マイクロタスクキュー」に入ります。

“D” を出力し、グローバルコードの実行が終わります。
ここでスタックが空になったので、イベントループが動きます。

イベントループのルールは、

「次の通常タスクに進む前に、マイクロタスクキューを全部処理する」

なので、まずマイクロタスクキューから C: promise を取り出してスタックに積み、実行します。
それが終わって初めて、通常のタスクキューの B: timeout が実行されます。

ここが重要です。
イベントループは、“1つのタスク → その後にマイクロタスクを全部 → 次のタスク → …” という順で動いている。
だから Promise.then の処理は、setTimeout のコールバックよりも優先される。


具体例3:イベントループと「画面更新」(UI)の関係

重い同期処理があると何が起こるか

例えばブラウザで次のようなコードを動かします。

const button = document.querySelector("#btn");

button.addEventListener("click", () => {
  console.log("クリック開始");

  const start = Date.now();
  while (Date.now() - start < 3000) {
    // 3秒間重い処理を続ける
  }

  console.log("クリック処理終了");
});
JavaScript

ボタンをクリックすると、
“クリック開始” が出てから 3 秒間、ブラウザがほぼフリーズし、
その後 “クリック処理終了” が出ます。

この 3 秒間、イベントループの視点では何が起きているでしょうか。

クリックされた瞬間、「クリックイベントのコールバック」がタスクキューから取り出され、スタックに積まれて実行されます。
その関数の中で重い while ループが 3 秒間走り続けます。
この間、スタックは「クリックコールバック」で占拠されているため、
イベントループはタスクキューやマイクロタスクキューを見に行っても、
「スタックが空いていないから新しい仕事を積めない」状態になっています。

結果として、
他のクリックやスクロールのイベント、setTimeout のコールバック、Promise.then の処理など、
すべてタスクキューやマイクロタスクキューに溜まるだけで、実行されません。

ここが重要です。
イベントループがどれだけ優秀でも、
スタックを長時間占有する同期処理があると、その間は「次のタスク」に進めない。
これが“ブロッキング”であり、UI が固まる根本原因です。


イベントループを自分の頭の中でシミュレーションする

小さな例で「今スタックは? キューは?」を追う

次のコードを見て、ログと一緒に「今の時点でスタック・タスクキュー・マイクロタスクキューがどうなっているか」を紙に書き出してみてください。

console.log("1");

setTimeout(() => {
  console.log("2: timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("3: promise");
});

console.log("4");
JavaScript

おおよその流れはこうなります。

最初に「1」が出る(同期)。
setTimeout は Web API に仕事を渡し、完了したらタスクキューにコールバックを置く準備をする。
Promise.resolve().then は、then のコールバックをマイクロタスクキューに積む。
「4」が出る(同期)。
グローバルコードが終わり、スタックが空になる。
イベントループがマイクロタスクキューを見て、「3: promise」を実行する。
それが終わってから、タスクキューの「2: timeout」を実行する。

こうやって、
「今どのタイミングでどのキューに何が入るのか」「スタックが空いたときにイベントループがどこを見るのか」を追いかける練習をすると、
非同期コードの挙動を「感覚」で読めるようになっていきます。


まとめ:イベントループを一文で言い直す

イベントループの本質は、
「コールスタック・タスクキュー・マイクロタスクキュー・Web API の間をぐるぐる回りながら、『今できる次の仕事』をシングルスレッドの JavaScript に途切れなく渡し続ける仕組み」
です。

押さえておきたいポイントを整理すると、

イベントループは、スタックが空いたタイミングで
「マイクロタスクキューを全部 → 次にタスクキューから1つ」と仕事を取り出して実行する。
setTimeout や DOM イベントのコールバックは通常のタスクキューに、Promise.then や async/await の続きはマイクロタスクキューに入る。
スタックを長時間専有する同期処理があると、その間イベントループは「次のタスク」を始められず、UI が固まる。

まずは、
console.logsetTimeoutPromise+イベントリスナーを組み合わせた小さな実験コードを書き、
「これは今どのキューに入る? いつスタックに乗る?」と頭の中でイベントループをシミュレーションしてみてください。

イベントループがイメージできるようになると、
JavaScript の非同期処理は「暗記するルール」から、「頭の中で動かせる仕組み」に変わります。

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