🚀 JavaScript Event Loop & Concurrency Model: A Beginner's Guide

"JavaScript はシングルスレッドでありながら、タイマー、API リクエスト、ユーザー操作、非同期タスクをアプリケーションをフリーズさせることなく処理できます。これはどのようにして可能なのでしょうか? その答えは Event Loop と JavaScript の Concurrency Model にあります。"

JavaScript を学び始めた当初、常に私を困惑させていた質問がありました:

JavaScript が一度に 1 つのタスクしか実行できない場合、API 呼び出し、タイマー、ユーザー操作などの複数のタスクをどのように同時に実行するのでしょうか?

答えは JavaScript の Event Loop と Concurrency Model です。

この記事では、JavaScript が舞台裏で非同期操作をどのように管理しているのかを、シンプルな説明、図、実際の例を用いて理解していきます。


📚 学習内容

  • JavaScript の Concurrency Model とは?
  • なぜ JavaScript はシングルスレッドなのか
  • Call Stack
  • Web APIs / Node APIs
  • Callback Queue (Macrotask Queue)
  • Microtask Queue
  • Event Loop
  • setTimeout() と Promise の違い
  • Cooperative Concurrency
  • Concurrency と Parallelism の違い

始めましょう!


🧵 JavaScript はシングルスレッド

JavaScript は シングルスレッド を使用してコードを実行します。つまり、一度に 1 つの操作しか実行できません。

1 人でキッチンで働くシェフを想像してみてください。

シェフが 1 つの料理の調理を始めたら、最初の料理が完成するまで別の料理を調理することはできません。

同様に、JavaScript は一度に 1 つのステートメントを実行します。

例:

console.log("Task 1");
console.log("Task 2");
console.log("Task 3");

Enter fullscreen mode Exit fullscreen mode

出力

Task 1
Task 2
Task 3

Enter fullscreen mode Exit fullscreen mode

すべてが順番に実行されます。


🤔 では、JavaScript はどのように複数のタスクを処理するのでしょうか?

現代のアプリケーションでは次のような処理が必要です:

  • サーバーからデータを取得する
  • タイマーを待つ
  • ファイルを読み込む
  • ボタンのクリックを処理する
  • 動画を再生する

すべての操作が完了するまで JavaScript が待機していたら、Web ページはフリーズしてしまいます。

代わりに、JavaScript は Concurrency Model を使用します。


⚙️ Concurrency Model とは?

Concurrency Model により、JavaScript はメインスレッド上で同時に実行することなく、複数のタスクを効率的に管理 できます。

JavaScript は時間のかかる操作をブラウザー (Web APIs) または Node.js (Node APIs) に委譲します。これらの操作が JavaScript エンジンの外で実行されている間、メインスレッドは他のコードの実行を継続します。

非同期タスクが完了すると、そのコールバックはキューに入れられ、後で Event Loop によって実行されます。


🧱 Call Stack

Call Stack とは?

Call Stack は、関数の実行を追跡するデータ構造です。

LIFO (Last In, First Out) の原則に従います。

関数が呼び出されたとき:

  • スタックにプッシュされます。
  • 完了するとポップされます。


function first() {
    console.log("First");
}

function second() {
    first();
    console.log("Second");
}

second();

Enter fullscreen mode Exit fullscreen mode

出力

First
Second

Enter fullscreen mode Exit fullscreen mode

実行

Call Stack

↓

second()

↓

first()

↓

console.log()

↓

first() removed

↓

console.log()

↓

second() removed

Enter fullscreen mode Exit fullscreen mode


🌐 Web APIs / Node APIs

JavaScript 自体はタイマーやネットワークリクエストなどの操作を実行できません。

これらの機能は環境によって提供されます。

ブラウザー Web APIs

  • setTimeout()
  • setInterval()
  • fetch()
  • DOM Events
  • localStorage
  • Geolocation

Node.js APIs

  • File System (fs)
  • HTTP Server
  • Streams
  • Timers
  • Process APIs

これらの API は非同期処理を実行し、その間 JavaScript は他のコードの実行を継続できます。


console.log("Start");

setTimeout(() => {
    console.log("Timer Completed");
}, 2000);

console.log("End");

Enter fullscreen mode Exit fullscreen mode

出力

Start
End
Timer Completed

Enter fullscreen mode Exit fullscreen mode

JavaScript が 2 秒待機しないことに注目してください。


📥 Callback Queue (Macrotask Queue)

非同期操作が完了すると、そのコールバックは Callback Queue (別名 Macrotask Queue) に配置されます。

Macrotask の例

  • setTimeout()
  • setInterval()
  • DOM Events
  • setImmediate() (Node.js)


console.log("Start");

setTimeout(() => {
    console.log("Timeout");
}, 0);

console.log("End");

Enter fullscreen mode Exit fullscreen mode

出力

Start
End
Timeout

Enter fullscreen mode Exit fullscreen mode

タイマーの遅延が 0 ミリ秒 でも、コールバックは Call Stack が空になるまで実行されません。


⚡ Microtask Queue

Microtask Queue は優先度の高い非同期タスクを保持します。

例:

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • queueMicrotask()

Microtask は常に macrotask より 先に 実行されます。


console.log("Start");

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

console.log("End");

Enter fullscreen mode Exit fullscreen mode

出力

Start
End
Promise

Enter fullscreen mode Exit fullscreen mode


🔄 Event Loop

Event Loop とは?

Event Loop はスケジューラーです。

その役割は Call Stack を継続的に監視することです。

Call Stack が空になると、以下を確認します:

  1. Microtask Queue
  2. Callback Queue

Event Loop はまず すべての microtask を実行します。

Microtask Queue が空になってから、1 つの macrotask を実行します。


Event Loop のワークフロー

JavaScript Code
        │
        ▼
   Call Stack
        │
        ▼
Async Operation
        │
        ▼
Web APIs / Node APIs
        │
        ▼
 ┌──────────────────┐
 │ Microtask Queue  │
 │ Promise.then()   │
 │ queueMicrotask() │
 └──────────────────┘
 ┌──────────────────┐
 │ Callback Queue   │
 │ setTimeout()     │
 │ DOM Events       │
 └──────────────────┘
        │
        ▼
    Event Loop
        │
        ▼
    Call Stack

Enter fullscreen mode Exit fullscreen mode


🆚 setTimeout() と Promise の比較

よくある面接質問の 1 つが setTimeout() と Promise の実行順序です。


console.log("Start");

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

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

console.log("End");

Enter fullscreen mode Exit fullscreen mode

出力

Start
End
Promise
setTimeout

Enter fullscreen mode Exit fullscreen mode


なぜ Promise が先に実行されるのか?

実行手順を 1 つずつ見ていきましょう。

手順 1

console.log("Start");

Enter fullscreen mode Exit fullscreen mode

出力

Start

Enter fullscreen mode Exit fullscreen mode


手順 2

setTimeout(...)

Enter fullscreen mode Exit fullscreen mode

タイマーは Web API に登録されます。


手順 3

Promise.resolve().then(...)

Enter fullscreen mode Exit fullscreen mode

コールバックは Microtask Queue に配置されます。


手順 4

console.log("End");

Enter fullscreen mode Exit fullscreen mode

出力

End

Enter fullscreen mode Exit fullscreen mode


手順 5

Call Stack が空になります。

Event Loop はまず Microtask Queue を確認します。

出力

Promise

Enter fullscreen mode Exit fullscreen mode


手順 6

すべての microtask が完了した後、Event Loop は Callback Queue を実行します。

出力

setTimeout

Enter fullscreen mode Exit fullscreen mode


🤝 Cooperative Concurrency

JavaScript は cooperative concurrency を使用します。

これは以下を意味します:

  • 一度に 1 つの JavaScript コードのみが実行されます。
  • タスクは完了後に自発的に制御を譲渡します。
  • タスクは他のタスクを中断しません。


console.log("Task 1");

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

console.log("Task 3");

Enter fullscreen mode Exit fullscreen mode

出力

Task 1
Task 3
Task 2

Enter fullscreen mode Exit fullscreen mode


⚡ Concurrency と Parallelism の違い

多くの初心者はこれらの用語が同じ意味だと考えますが、異なります。

Concurrency

複数のタスクを効率的に切り替えて管理すること。

例:

複数の料理を切り替えながら準備するシェフ。

Parallelism

複数のワーカーまたは CPU コアを使用して、複数のタスクを完全に同時に実行すること。

例:

3 人のシェフが 3 つの異なる料理を同時に調理する。


JavaScript と Parallelism

JavaScript のメインスレッドは 並列ではありません

ただし、以下を使用して真の並列処理を実現できます:

  • Web Workers (ブラウザー)
  • Worker Threads (Node.js)

これらは CPU 負荷の高い作業用に別個のスレッドを作成します。


📊 Microtask Queue と Callback Queue の比較

機能 Microtask Queue Callback Queue
優先度 高い 低い
Promise.then(), catch(), finally(), queueMicrotask() setTimeout(), setInterval(), DOM Events
実行 Call Stack が空になった直後に実行 すべての Microtask が完了した後に実行

📝 重要なポイント

  • JavaScript は シングルスレッド です。
  • Call Stack は同期コードを実行します。
  • Web APIsNode APIs は非同期操作を処理します。
  • 完了した非同期コールバックはキューに配置されます。
  • Microtask QueueCallback Queue より優先度が高いです。
  • Event Loop は Call Stack を継続的に確認し、タスクをスケジュールします。
  • Promise は常に setTimeout() コールバックより先に実行されます。microtask が先に処理されるためです。
  • JavaScript は Event Loop によって concurrency を実現し、真の parallelism には Web Workers または Worker Threads が必要です。

🎯 結論

Event Loop と JavaScript の Concurrency Model を理解することは、効率的な非同期コードを書くために不可欠です。Call Stack、Web APIs、タスクキュー、Event Loop などの概念は、JavaScript がタイマー、ネットワークリクエスト、ユーザー操作を処理しながらも応答性を保てる理由を説明します。

これらの基本を理解すれば、async/await、API 呼び出し、React や Node.js などのモダンフレームワークなどのトピックはすべて同じ非同期実行モデルに基づいているため、はるかに学びやすくなります。