🚀 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 が空になると、以下を確認します:
- Microtask Queue
- 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 APIs と Node APIs は非同期操作を処理します。
- 完了した非同期コールバックはキューに配置されます。
- Microtask Queue は Callback 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 などのモダンフレームワークなどのトピックはすべて同じ非同期実行モデルに基づいているため、はるかに学びやすくなります。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.