Kodokon kodokon.com

イベントループ: コールスタック、マイクロタスク、マクロタスク

エンジンが同期コード、マイクロタスク、マクロタスクを実際にどうスケジュールするのかを理解します。

10 分 · 3 問

このレッスンを Kodokon で開く

JavaScript はコードを単一のスレッドで実行しますが、イベントループは ECMAScript で定義されているわけではありません。その動作を記述しているのは HTML 仕様(そして Node.js 側では libuv)です。エンジンは、現在実行中のコードのためのコールスタックマクロタスク(タイマー、I/O、イベント)のキュー、そしてマイクロタスクPromise.prototype.thenqueueMicrotaskMutationObserver)のキューを保持します。黄金律はこうです。各マクロタスクの後、制御が返される前にマイクロタスクキューは完全に空にされる、ということです。

JAVASCRIPT
console.log("script start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

console.log("script end");
// script start, script end,
// promise 1, promise 2, timeout
マイクロタスクは常にマクロタスクより先に実行されます。

最初のスクリプト自体が 1 つのマクロタスクです。それが終わるまで、他には何も実行されません。コールスタックが空になると、エンジンはマイクロタスクキューを空にします。空にしている最中に追加されたマイクロタスクも含めてです。この詳細が、連鎖した 2 つの then 呼び出しが setTimeout(fn, 0) より先に実行される理由を説明します。各 then は別のマイクロタスクをスケジュールし、キューが空になるまで制御は決して返されないのです。

JAVASCRIPT
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);

queueMicrotask(() => {
  console.log("micro 1");
  queueMicrotask(() => console.log("micro 2"));
});
// micro 1, micro 2, macro 1, macro 2
ネストされたマイクロタスクは、どのマクロタスクよりも先に実行されます。

await キーワードはこの仕組みの上に成り立っています。await は async 関数を中断し、呼び出し元に制御を返してから、再開をマイクロタスクとしてスケジュールします。await null でさえこの中断を引き起こします。なぜなら、その値は解決済みの Promise でラップされるからです。これが、await の後に置かれたコードが決して同期的には実行されない理由です。

JAVASCRIPT
async function main() {
  console.log("before await");
  await null;
  console.log("after await");
}

main();
console.log("sync code");
// before await, sync code, after await
await は関数を中断し、残りを再スケジュールします。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. あるスクリプトが C を同期的にログ出力し、A を setTimeout(fn, 0) で、B を Promise.resolve().then でスケジュールします。文字が現れる順序はどれですか?
    • A, B, C
    • C, A, B
    • C, B, A
    • B, C, A
  2. マイクロタスクキューはいつ空にされますか?
    • 各マクロタスクの後に、空にしている最中に追加されたマイクロタスクも含めて完全に
    • 2 つのマクロタスクの間にマイクロタスクは 1 つだけ実行される
    • ページが描画される直前、16 ms ごとに
    • すべてのマクロタスクが処理されてから一度だけ
  3. await は内部で実際に何をしていますか?
    • Promise が解決するまでスレッドをブロックする
    • async 関数を中断し、その再開をマイクロタスクとしてスケジュールする
    • 関数の残りをマクロタスクに変える
    • await される値が Promise でなければ何もしない