まず「実行順序の全体像」を一言でまとめる
JavaScript の実行順序は、ざっくり言うとこうなります。
同期処理(ふつうのコード)を上から順番に実行する
同期が一段落してコールスタックが空いたら、Promise 関連(マイクロタスク)を全部片付ける
それも終わったら、タイマーやイベントのコールバック(タスク)を一つだけ実行する
また同期 → マイクロタスク → タスク…というサイクルを延々繰り返す
この「サイクル」を回しているのがイベントループです。
ここが重要です。
「同期 → マイクロタスク → タスク」という 3 段階が見えてくると、
非同期コードの実行順序は「覚えるもの」から「説明できるもの」に変わります。
主役たちの位置づけを一度整理する
コールスタックが「今まさに実行されている場所」
コールスタックは、実行中の関数の積み重ねでした。
グローバルコードがまず積まれ、その中で関数を呼べば、その関数が上に積まれていきます。
終われば上から順に消えていき、全部終わればスタックは空になります。
「今どこを実行しているか」を決めるのは、このコールスタックです。
実行順序を考えるときは、
「いまスタックに何が乗っていて、それがいつ空になるか」を意識すると分かりやすくなります。
Web API が「外側の時間のかかる仕事」を担当する
setTimeout, fetch, DOM イベントなどの「時間のかかる処理」は、
JavaScript 本体ではなく Web API 側が担当します。
JavaScript は「これお願いします」と依頼し、
それが完了したら Web API が「このコールバックをキューに入れておいて」と伝えます。
非同期処理が「いつキューに積まれるか」は Web API 側が決め、
「いつ実行されるか」はイベントループとキューのルールで決まります。
タスクキューとマイクロタスクキューが「あとでやる処理」を並べる
非同期処理のコールバックは、直接すぐには実行されず、まずキューに並びます。
通常のタスクキューには、setTimeout, setInterval, DOM イベントなどのコールバックが積まれます。
マイクロタスクキューには、Promise.then, catch, finally や async/await の続き、queueMicrotask で登録された処理が積まれます。
この二つのキューは、イベントループから見ると「優先度の違う順番待ちの列」です。
イベントループが「次に実行する仕事」を決め続ける
イベントループは、ずっとこう考え続けています。
スタックは空か?
空いたなら、まずマイクロタスクキューに何かあるか?
あれば全部処理する。
それも終わったら、タスクキューから一つだけ取り出して実行する。
この繰り返しが、JavaScript の「実行順序」を作り出しています。
一番シンプルな実行順序:同期コードだけの世界
まずは非同期なし、純粋な同期処理だけで考えます。
function foo() {
console.log("foo start");
bar();
console.log("foo end");
}
function bar() {
console.log("bar");
}
console.log("global start");
foo();
console.log("global end");
JavaScript実行結果は、
global start
foo start
bar
foo end
global end
という順番になります。
ここでは、Web API もタスクキューもマイクロタスクも出てきません。
ただ、コールスタックの中で、
グローバル → foo → bar → foo に戻る → グローバルに戻る
という流れをたどっているだけです。
この「1 本のスタックを上から下まで辿る」感覚が、実行順序の出発点です。
setTimeout を混ぜたときの実行順序
例1:setTimeout と同期コード
console.log("A");
setTimeout(() => {
console.log("B: timeout");
}, 0);
console.log("C");
JavaScript結果は必ず、
A
C
B: timeout
になります。
なぜかを、さっきの 3 段階に当てはめてみます。
同期フェーズでは、グローバルコードが実行され、”A” → setTimeout 呼び出し → “C” と進みます。setTimeout は Web API に仕事を渡すだけで、スタック上ではすぐに終わります。
グローバルコードがすべて終わるとスタックが空になります。
次にマイクロタスクフェーズですが、この例では Promise などがないのでマイクロタスクキューは空です。
最後にタスクフェーズで、タスクキューに入っている setTimeout のコールバックが一つ取り出され、”B: timeout” が実行されます。
このサイクルが一回回って、実行順序が決まるわけです。
Promise を混ぜるとどう変わるか
例2:Promise と setTimeout の組み合わせ
console.log("A");
setTimeout(() => {
console.log("B: timeout");
}, 0);
Promise.resolve().then(() => {
console.log("C: promise");
});
console.log("D");
JavaScript多くの環境での結果は、
A
D
C: promise
B: timeout
になります。
これも 3 フェーズで追ってみましょう。
同期フェーズでは、”A” → setTimeout → Promise.resolve().then(...) → “D” と進みます。
ここで setTimeout はタスクキュー行きの予約をし、Promise.resolve().then はマイクロタスクキューに「C: promise」の処理を積みます。
グローバルコードが終わると、スタックが空になります。
マイクロタスクフェーズでは、マイクロタスクキューに入っている C: promise を全部実行します。
このとき “C: promise” が出力されます。
その実行の中で新たにマイクロタスクを登録すれば、それもこのフェーズで処理されます。
マイクロタスクが全部終わってから、タスクフェーズに進みます。
ここでタスクキューから B: timeout を実行し、”B: timeout” が出力されます。
この流れを一度頭の中で辿ってみると、
「Promise の then が setTimeout より先に実行される理由」が、暗記ではなく構造として理解できてきます。
async/await が入ってきたときの実行順序
例3:async/await と同期コード
async function main() {
console.log("1");
await Promise.resolve();
console.log("2");
}
console.log("A");
main();
console.log("B");
JavaScript結果は、
A
1
B
2
のようになります。
最初の “A” は、グローバルコードの一部として同期的に実行されます。main() を呼ぶと、main 関数が同期的に動き始め、”1″ が出力されます。await Promise.resolve() に到達すると、その Promise がすぐに解決されるとともに、「await のあとの続き」がマイクロタスクとして予約されます。
ここで main の実行はいったん終了し、関数から抜けます(スタックから main が消えるイメージです)。
グローバルコードに戻り、”B” が出力されます。
グローバルコードが終わってスタックが空になると、マイクロタスクフェーズが始まり、main の続き(”2″ を出力する部分)が実行されます。
ここが重要です。
async/await は「見た目は同期っぽい」ですが、await のあとの処理は「マイクロタスクキューから実行される別タスク」 です。
だからこそ "B" のあとに "2" が出てくるわけです。
実行順序を決める「3段階サイクル」を言語化する
これまでの例をまとめると、JavaScript はだいたい次のようなサイクルで動きます。
最初に、グローバルコード(ファイルのトップレベル)がコールスタックに積まれ、同期処理が上から順に実行される。
同期処理の中で、setTimeout や fetch を呼ぶと Web API に仕事が渡され、結果が出たときにコールバックがタスクキューに積まれる。
Promise.resolve().then, async/await のような処理は、then や続きがマイクロタスクキューに積まれる。
グローバルコードや現在のタスクが終わってコールスタックが空になると、イベントループが「マイクロタスクキューに入っているもの全部」を先に実行する。
マイクロタスクがすべて終わったら、今度はタスクキューから 1 つだけ取り出して実行し、また同期処理 → マイクロタスク → …という流れに入る。
この「同期 → マイクロタスク → タスク → 同期 → …」のサイクルが、
JavaScript の「実行順序の全体像」です。
実行順序を自分で追うための考え方
コードを見たときに、まずやること
非同期が混ざったコードを読んだとき、次の順番で考える癖をつけると整理しやすいです。
上から順に見て、「同期的に実行される部分」をマークする
その中で、どこで「Promise(マイクロタスク)」が登録されているかを見つける
どこで「setTimeout やイベント(タスク)」が登録されているかを見つける
同期部分が終わったとき、「マイクロタスクキューには何がいる?」「タスクキューには何がいる?」と考える
マイクロタスク → タスクの順で「誰から実行されるか」を並べてみる
最初は紙に書いてもいいです。
慣れてくると、頭の中で「あ、ここで then がマイクロタスクに積まれるな」「こっちは setTimeout だから次のタスクだな」とシミュレーションできるようになります。
小さい例で練習してみると良いパターン
例えば次のようなコードを自分で書いて、順番を予想してからコンソールで確かめてみてください。
同期ログをいくつかsetTimeout(..., 0) を複数Promise.resolve().then を複数async/await を混ぜた関数
このあたりを試すだけで、
「実行順序のルール」がだいぶ自分の感覚として馴染んできます。
まとめ
「実行順序の全体像」を一言で言うと、
JavaScript は、同期コードをまず実行し、終わったタイミングでマイクロタスク(Promise/await など)をすべて片付け、そのあとで通常のタスク(setTimeout/イベント)を一つずつ処理する。
この流れをイベントループがぐるぐる回し続けている。
ということです。
押さえておきたいポイントは、
同期処理はコールスタックの中で、上から順番に実行される
非同期処理はすぐには実行されず、まずタスクキューかマイクロタスクキューに積まれる
スタックが空になったタイミングで、まずマイクロタスクを全部、そのあと通常タスクを一つ実行する
Promise / async/await は「優先度高め」のマイクロタスク、setTimeout やイベントは「通常タスク」
この 4 つです。
ここまでの話がつながると、
「なぜその順番で実行されるのか」を、自分で説明できるようになります。
あとは実際に手を動かして、小さな実験を繰り返してください。
実行順序を何度か「予想 → 実行 → 確認」しているうちに、
イベントループの動きが、だんだん「頭の中で見える」ようになっていきます。
