JavaScriptはなぜ「止まらない」のか? 🤔

JavaScriptはシングルスレッド言語ですが、Webサイトは多数のネットワークリクエスト、タイマー、ユーザーイベントを同時に処理し、停止することはありません。この魔法のような並行処理(concurrency)の秘密は、イベントループ(Event Loop) システムにあります。本ガイドでは、コールスタック(Call Stack)、Web API、タスクキュー(Task Queue)、マイクロタスクキュー(Microtask Queue)がどのように連携して非同期タスクを処理するかを段階的に分析します。この原理を理解することは、面接対策だけでなく、複雑なアプリケーションの動作を予測しデバッグするシニア開発者の思考法を身につける第一歩です。

JavaScript Event Loop diagram showing call stack and queue interaction Future Tech Concept

非同期処理を支える4つの核心要素 🧩

コールスタック (Call Stack) - 実行の基本単位

JavaScriptエンジンは単一のコールスタックを持ちます。全ての関数呼び出しはこのスタックにpushされ、実行が完了するとpopされます。これによりコードが順次実行されることが保証されます。

Web API - 外部世界との接続点

setTimeoutfetchDOMイベントリスナーなどの機能は、JavaScriptエンジン自体の機能ではなく、ブラウザまたはNode.jsランタイムが提供するWeb APIです。これらのAPIは時間のかかる処理(タイマー、ネットワークリクエスト)をコールスタックの外部で処理し、メインスレッドがブロックされるのを防ぎます。

タスクキュー & マイクロタスクキュー - 二種類の待合室

Web APIで処理が完了すると、コールバック関数はすぐにコールスタックへは戻らず、キュー(Queue)に格納されます。ここで重要なのは、キューに二種類あるという点です。

graph TD
    A[Call Stack] -->|Empty?| B{Event Loop}
    B -->|Yes| C[Microtask Queue]
    B -->|No| A
    C -->|Higher Priority| A
    D[Task Queue] -->|Lower Priority| B
    E[Web APIs] -->|Callback Ready| D
    F[Promise/MutationObserver] -->|Callback Ready| C

Visual representation of microtask queue and callback queue priority IT Gadget Setup

イベントループの動作メカニズムと優先順位 🎯

マイクロタスクキューの絶対的優先権

イベントループはコールスタックが空になった瞬間、常にマイクロタスクキューを最初に確認します。マイクロタスクキューが完全に空になるまで、タスクキューは実行されません。この優先順位は、PromiseMutationObserverqueueMicrotask() APIによって生成されたコールバックに適用されます。

関数のスターベーション (Starvation)

もしマイクロタスクキューに新しいタスクが次々と追加され続けると(例: Promiseチェーン内で新しいPromiseが生成される)、タスクキューにあるsetTimeoutのコールバックは永遠に実行されない可能性があります。これを関数のスターベーションと呼び、フロントエンドの面接で頻出の重要な概念です。

実行順序比較表

区分マイクロタスクキュー (Microtask Queue)タスクキュー / コールバックキュー (Task Queue)
優先順位高い(先に実行)低い(マイクロタスクキューが空になった後)
含まれるAPIPromise.then(), .catch(), .finally(), MutationObserver, queueMicrotask(), async/await 内部コードsetTimeout(), setInterval(), fetch() (ネットワーク応答), DOMイベントリスナー, setImmediate() (Node.js)
特徴一つずつ処理され、キューが完全に空になるまで次のタスクキューに移行しない一度に一つのコールバックを取り出して実行

コード例で見る実行順序

console.log('1. 同期コード');

setTimeout(() => console.log('2. タスクキュー'), 0);

Promise.resolve().then(() => console.log('3. マイクロタスクキュー'));

console.log('4. 同期コード終了');

// 出力結果:
// 1. 同期コード
// 4. 同期コード終了
// 3. マイクロタスクキュー
// 2. タスクキュー

上記の例では、setTimeoutの遅延時間が0msであっても、Promiseのコールバックが先に実行されることが確認できます。これはマイクロタスクキューがタスクキューよりも常に高い優先順位を持つためです。RedditのJavaScriptコミュニティでは、この優先順位の違いを理解していないために発生する予期せぬバグについての議論が頻繁に行われています。

Asynchronous JavaScript code execution flow diagram Hardware Related Image

まとめ: 非同期JavaScriptを支配する4つの法則 📝

  1. 全ての同期コードはコールスタックで即座に実行されます。
  2. Web APIが時間のかかる処理を代行し、メインスレッドのブロッキングを防ぎます。
  3. 処理完了後のコールバックは、その種類に応じてマイクロタスクキュー(高優先度) またはタスクキュー(低優先度) に格納されます。
  4. イベントループはコールスタックが空になった時、マイクロタスクキューを完全に空にしてからタスクキューを処理します。

この4つの法則を理解すれば、async/awaitPromisesetTimeoutが混在する複雑なコードの実行結果を正確に予測できるようになります。

📅 情報基準日: 2024-05-24

関連記事

Server-side JavaScript runtime environment visualization Smart Life Concept

本コンテンツは、信頼性の高い情報源をもとにAIツールを活用して作成され、編集者によるレビューを経て公開されています。専門家によるアドバイスの代替となるものではありません。