📘

JSの非同期処理、実は『交通整理』だった?自作図解でスタック・キュー・イベントループを完全攻略

に公開

概要

みなさんこんにちわ!
都内でWEBエンジニアとして働いているのですが、JSのことについてわかったつもりでいた非同期処理について図を使って整理したいと思います。
この記事を読むと以下の内容が整理できます。

  • JavaScriptが「シングルスレッド」でどうやってプログラムの実行をこなしているのか?
  • なぜ「通信を待っている間」もブラウザが固まらないのか?

JavaScriptの処理の流れを整理

用語の説明・解説

  1. 実行の入り口
    JavaScriptのコード (①) あなたが書いたソースコードそのものです。
    ここから全てが始まり、命令が一つずつ「スタック」へと送り込まれます。

  2. 実行の現場
    スタック (Stack) 現在進行形で実行されている関数が積まれる場所です。
    「机の上」のようなもので、ここに置かれた仕事を作業員(メインスレッド)が処理します。

  3. フレーム (Frame)
    スタックの中に積まれる、関数一つひとつの実行単位です。図の②にあるように、どの関数がどの関数を呼んだかという「実行の履歴」も管理しています。

  4. ヒープ (Heap)
    オブジェクトや変数などのデータが保存される広大な「倉庫」です。スタック上の「フレーム」は、ここにあるデータを参照(②の矢印)して計算を行います。

  5. 外注先
    WEB API (ブラウザ) (③・④) 通信(fetch)やタイマー(setTimeout)、イベント待機などをJavaScriptの外側(ブラウザ)に依頼する場所です。JavaScript自体は一人しか作業員がいませんが、ブラウザに「外注」することで、複数の作業を並行して進めることができます。

  6. 予約リスト(待機場所)
    マイクロタスクキュー (Microtask Queue) (⑤) Promiseの解決結果などが並ぶ**「優先予約リスト」**です。スタックが空になったら、真っ先にここから仕事が取り出されます。

  7. タスクキュー (Task Queue / Callback Queue)
    (⑥) setTimeout や addEventListener などの結果が並ぶ**「通常予約リスト」**です。マイクロタスクキューが完全に空にならない限り、こちらの仕事は順番が回ってきません。

  8. 交通整理の主役
    イベントループ (Event Loop) 「スタックの管理を行なっている司令塔」です。常にスタックを見張り、空になった瞬間にキューから予約(⑤や⑥)を取り出して、スタックへ運び込みます。

実行の順番

同期処理(例:console.log)

同期処理は以下の順番で実行されます。

  • ルート: コード → スタック → 完了。
  • 特徴: 他の箱を介さず、最速で処理される。

Promise / async await

Promiseやasync awaitの処理は同期処理とは異なります。

  • ルート: スタック → Web API → マイクロタスクキュー → スタック。
  • 特徴: 「優先チケット」を持っている。タスクキューより先に処理される。

addEventListener / setTimeout

こちらは、Promise / async awaitと基本の動きは同じです。

  • ルート: スタック → Web API → タスクキュー → スタック。
  • 特徴: マイクロタスクが空になるまで待たされる。

サンプルコード

実際にWEBブラウザの開発者コードを実行すると、1 → 2 → 4 → 5の順番で出力されます。
ここで一度図を確認しながらコードの処理を追っていくと整理がしやすいです。

console.log("1: 同期(スタック)");

setTimeout(() => {
  console.log("5: setTimeout(タスクキュー)");
}, 0);

Promise.resolve().then(() => {
  console.log("4: Promise(マイクロタスクキュー)");
});

console.log("2: 同期(スタック)");

番外編 async awaitの動き

async/await は Promise を「一時停止ボタン」に変える魔法のようなものだと言われますが、「await は JavaScript の処理を完全に止めているわけではない」 みたいです。

await したとき、図の中で何が起きている?
await が現れると、JavaScript は以下のような「鮮やかな離れ業」をやってのけます。

関数の脱出(スタックの解放) await というキーワードに出会った瞬間、その関数は実行を中断し、スタックから一旦消え去ります。 これが最大のポイントです。作業員(メインスレッド)は自由になり、他のクリックイベントやアニメーションの処理に戻ることができます。

Web API への依頼 await の後ろにある非同期処理(例:fetch)は、図の右側にある Web API へと投げられます。

マイクロタスクキューへの「予約」 await より下に書かれたコード(残りの処理)は、すぐには実行されません。これらは 「マイクロタスクキューへの予約チケット」 となり、Web API から「終わったよ!」という報告が届くのを待つようです。

async function getRecipe() {
  console.log("1: 注文を受けました"); // 同期処理(スタック)

  // ここで await! 関数を一時中断してスタックを空ける
  const data = await fetch("/api/recipe"); 

  // --- ここから下は「未来」の処理(マイクロタスクキュー) ---
  console.log("2: レシピが届きました", data); 
}

console.log("3: 別の作業(掃除など)をします");
getRecipe();

終わりに

いかがだったでしょうか?
記事の図を使えば複雑な非同期処理も整理すれば怖くないと思います。
みなさんもこれで非同期マスターに慣れたと思います!!!
ではまた^_^

Discussion