全体像
JavaScriptはシングルスレッドで動作し、コードの実行は常に「1つのコールスタック」上で順番に行われます。
非同期処理(タイマーやネットワーク呼び出し、Promiseなど)は、ブラウザ(ホスト環境)が用意する機能と、イベントループによるキューの管理によって並行的に扱われます。
おおまかには下記のような手順で、イベントループが繰り返し処理を行っています。
- コールスタック(Call Stack) 上でトップレベル(同期的)コードを実行
- 非同期処理があれば、ブラウザ側が該当処理(タイマー、通信など)を担当
- 処理結果のコールバックはキュー(Macro Task Queue / Microtask Queue)へ追加
- コールスタックが空になったタイミングで、Microtask Queue を優先的に処理
- Microtask Queueが空になったら、Macro Task Queue からタスクを1つ取り出して実行
- 再びマイクロタスクの有無を確認
- 以後、キューが空になるまで1〜6を繰り返す
以降のステップ詳細でより細かく見ていきます。
ステップバイステップ解説
ステップ1: コードの読み込みとトップレベルコードの実行
- ブラウザがHTMLを読み込み、
<script>
タグなどで指定されたJavaScriptファイルをロードします。 - JavaScriptエンジン(例: V8)がコードをパースし、**トップレベルコード(グローバルスコープで書かれた同期的処理)**を実行します。
- このとき実行中の処理は、Call Stack(コールスタック) に積まれます。
Call Stack: [ トップレベルのコード ]
- 同期的な処理は、関数呼び出しがあるたびにスタックに積まれ、処理が完了すればスタックから取り除かれます。
ステップ2: 非同期処理の委譲(ブラウザやNode.jsなどのホスト環境へ)
- トップレベルコードや関数内部で、
setTimeout()
やfetch()
などの非同期APIを呼び出した場合、その「待ち時間の計測」や「ネットワーク通信」などはブラウザのWeb APIやサーバサイドのNode.js内部のlibuvなどが担当します。 - つまり「JavaScriptエンジン自身」は重いI/O処理やタイマーを直接管理しません。呼び出したタイミングでブラウザやNode.jsに「あとでこのコールバックを呼んでね」と依頼する形になります。
- 依頼を受けたブラウザやNode.jsは、該当の処理が完了したらコールバック関数を「キュー」に登録する準備をします。
Call Stack: [ 実行中の関数 ]→非同期API呼び出し→(Web APIが処理継続)
- この時点では、Call Stack から見ると「非同期処理」はすぐに戻ってくるので、実行をブロックしません。
ステップ3: Call Stackが空になるまで同期処理を実行
- 同期的(ブロッキング)なコードがまだ残っていれば、引き続きそれらがCall Stack上で実行されます。
- 全部の同期処理が終わると、Call Stack は空(またはグローバルコンテキストだけ)になり、いわゆる「1度目の処理サイクル(タスク)」が完了します。
Call Stack:(空になる)
ステップ4: イベントループがタスクを取り出す(Microtask Queue → Macro Task Queue の順番)
4-1. Microtask Queueの処理
- イベントループは、Call Stackが空になったタイミングでまずはMicrotask Queue を確認します。
-
Promise
の.then()
やMutationObserver
、queueMicrotask()
などで登録されたコールバックは、このMicrotask Queueに積まれています。 - Microtask Queueにコールバックがあれば、先頭のコールバックを1つずつCall Stackに積んで実行します。
- 実行が終わった後、追加で新たなMicrotaskが発生した場合は、Queueが空になるまで実行を繰り返します。
Microtask Queue: [ 1st, 2nd, 3rd, ... ]→Call Stackで実行→再びMicrotask Queueを確認
- Microtaskは非常に優先度が高いタスクとして扱われ、次のMacro Taskに移る前に完全に処理される点が重要です。
4-2. Macro Task Queueの処理
- Microtask Queueが空になった後、今度は**Macro Task Queue(タスクキュー)**を確認します。
-
setTimeout()
やsetInterval()
、I/O
によるコールバック、ブラウザのDOMイベント(クリックなど)によるイベントハンドラ呼び出し、requestAnimationFrame
のコールバックなどは、一般的にMacro Task(または単にTask)として、このキューに積まれます。 - イベントループはこのMacro Task Queueから「もっとも先頭のタスク」を1つ取り出し、Call Stackに積んで実行を始めます。
Macro Task Queue: [ 1st, 2nd, 3rd, ... ]→Call Stackで実行
- 1つのMacro Taskが完了したタイミングで、再びMicrotask Queueを確認し、そこにあるタスクをすべて片付けたうえで、次のMacro Taskに進みます。
ステップ5: 描画(リペイント・再描画)との関係
ブラウザでは、「Macro Taskを1つ実行したあとに画面の描画(リペイント)を行う」といったタイミングをはさんでいます。
要素の位置や大きさを変更するDOM操作を行った場合も、実際の画面描画は基本的にタスクの実行が完了したタイミングなどでまとめて処理されます。
ステップ6: 繰り返し(ループ)の継続
- 上記のように「Call Stackが空になった → Microtask → Macro Task → Microtask → …」というサイクルを常に繰り返します。
- どこかで新しい非同期処理が発生(例えば
setTimeout
やネットワーク完了など)すると、そのコールバックがMacro Task Queue(またはMicrotask Queue)に追加されます。 - キューにタスクが存在する限り、このサイクルは止まらずに続きます。