Interactive

イベントループ可視化

JavaScriptの実行順は気分や運では決まりません。WHATWG HTML仕様が「タスクを1個実行 → マイクロタスクを空になるまで全消化 → 必要なら描画」という1反復のアルゴリズムを厳密に定めています。 コールスタック・タスクキュー・マイクロタスクキュー・rAFコールバック・コンソール出力を並べて、 この反復を1ステップずつ再生します。なぜ setTimeout(…, 0) でも Promise に勝てないのかが、 構造から見えます。

記事の基本例。setTimeout(…, 0) でも Promise には勝てない理由が1反復の構造から分かる

対象のコード

console.log('A');                        // 同期
setTimeout(() => console.log('B'), 0);   // タスクキューへ
Promise.resolve()
  .then(() => console.log('C'));         // マイクロタスクキューへ
console.log('D');                        // 同期

仕様どおりの出力順

ADCB
コールスタック実行中はここが埋まる(1本だけ)
(空 = 次の仕事を取り出せる)
マイクロタスクキュー空になるまで全部0
(空)
タスクキュー(マクロタスク)1反復で1個だけ1
スクリプト全体(タスク)
rAFコールバックペイント直前に走る0
(空)

コンソール出力

(まだ何も出力されていない)
1 / 18
イベントループ1反復目

1. 初期状態:スクリプト全体が1個のタスク

ブラウザは読み込んだ `<script>` の実行そのものを「1つのタスク」として扱う。イベントループはまずこれをタスクキューから取り出す。

ここが分かる

  • すべての順序は「タスクは1反復で1個だけ、マイクロタスクは空になるまで全部」という非対称性から導かれる。setTimeout(…, 0) の 0 は「すぐ実行」ではなく「キューに積むだけ」——だからマイクロタスクに並んだ Promise には構造的に勝てない。
  • await は then の糖衣。「await で同期的に止まる」とイメージすると順序を読み違える。await より後ろのコードはマイクロタスクとして再開されるので、setTimeout より先に走る——「await は then の糖衣」シナリオで実際の並びを確認できる。
  • rAF はマクロタスクでもマイクロタスクでもない。描画機会(ステップ3)の冒頭、スタイル計算・ペイントの直前に走る専用の枠。だから rAF 内の DOM 変更は同じフレームの描画に間に合い、アニメーションで setTimeout より正しい。
  • マイクロタスク内でマイクロタスクを積み続けるとキューが永久に空にならず、次のタスクにも描画にも到達できない(=画面が固まる)。「マイクロタスクの飢餓」シナリオでは、タスクキューと rAF に積まれたものが最後まで実行されないまま残ることが確認できる。