JavaScript は単一スレッドで動くため、一度に1つのことしかできません。同期コードはコールスタック上で上から下へ実行されます。非同期のもの(setTimeout のコールバック、Promise の .then、click ハンドラー)はその場で実行されず、キューに置かれます。イベントループは「次に何を実行するか」を決める単純なルールです。コールスタックが空になるたびに、待っている次のタスクを取り出して実行し、それを繰り返します。このツールはそのループを一歩ずつ可視化します。
実はキューは2つあり、その優先順位こそがこの謎の核心です。Promise のコールバック(と queueMicrotask)は microtask キューへ、setTimeout・setInterval・ほとんどのイベントは macrotask キューへ入ります。ルールは、各 macrotask のあと(および最初のスクリプトのあと)、ループは次の macrotask を実行する前に microtask キュー全体を排出する、というものです。だから同じ遅延なら Promise の .then は常に setTimeout に勝ちます。microtask が先に空にされるからです。このツールで2つのキューを見れば、順序はもう謎ではなくなります。
このテーマに関するよくある疑問と回答をまとめました。
Promise のコールバックは microtask、setTimeout のコールバックは macrotask であり、イベントループは各 macrotask のあと(および最初のスクリプトのあと)、次の macrotask を実行する前に microtask キュー全体を排出するからです。だから 0ms の setTimeout でも macrotask キューで待たされ、保留中の microtask がすべて先に走ります。
Promise の反応(.then・.catch・.finally)、await の継続、queueMicrotask のコールバック、MutationObserver のコールバックは microtask です。setTimeout・setInterval・requestAnimationFrame・I/O・ほとんどの DOM イベントは macrotask です。この区別が重要なのは、ループが macrotask の間にすべての microtask を空にするため、microtask を延々と予約すると描画や後続のタイマーを飢餓状態にしうるからです。
いいえ。0ms の遅延はコールバックが「できるだけ早く実行できる」ことを意味するだけで、依然として macrotask キューに入り、現在の同期コードと保留中のすべての microtask が終わるまで待ちます。ブラウザは入れ子のタイマーを最小値(数階層後で約 4ms)に制限するため、0ms は下限であって即時呼び出しではありません。