3秒でわかる
実行中の処理が終わるたびに待ち行列から次の仕事を取り出す仕組み。1本のスレッドのまま、通信やタイマーの待ち時間を無駄にせず進められます。
もう少し詳しく
どういうものか
JavaScript は1本のスレッドでコードを実行します。にもかかわらず通信の完了やタイマーを待てるのは、待つ処理をブラウザや Node.js 側に預け、完了した順にコールバックを待ち行列へ積んでおく作りになっているからです。
イベントループは、今実行中の関数がすべて終わってコールスタックが空になった瞬間に、待ち行列から次のコールバックを1つ取り出して実行します。この巡回を止めずに続けているのがイベントループです。
[コールスタック] 空になった?
↑ ↓ はい
└── 実行 ←── [マイクロタスクキュー] Promise の then
↓ 空になったら
[タスクキュー] setTimeout, I/O, クリックなぜ必要か
通信の応答を「その場で待つ」作りにすると、待っている間そのスレッドは何もできません。画面はクリックに反応せず、サーバーなら他の利用者のリクエストが止まります。待ちを預けて先に進む設計にすることで、スレッドが1本でも数千の接続を同時に抱えられます。
具体例
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 出力は 1, 4, 3, 2setTimeout に 0 を渡しても2番目には出ません。同期処理がすべて終わり、さらに Promise のマイクロタスクを処理し終えてから、ようやくタイマーの番が来ます。
つまずきやすいところ
重い同期処理はすべてを止めます。巨大な配列のソートや同期版のファイル読み込みを書くと、その間はクリックも他のリクエストも処理されません。時間のかかる計算は分割するか、Web Worker や別プロセスへ逃がします。
setTimeout(fn, 0) は「今すぐ」ではなく「手が空いた次の周回で」です。描画の直後に何かしたい、という目的でこれを使うと、環境によって順序が揺れます。
Promise のコールバックはタイマーより優先されます。マイクロタスクの中でさらに Promise を作り続けると、タスクキューにいるクリック処理がいつまでも回ってこない状態を作れてしまいます。
似た用語との違い
| 用語 | 中身 | 実行の順番 |
|---|---|---|
| コールスタック | 実行中の関数の積み重ね | 常に最優先 |
| マイクロタスク | Promise の then, queueMicrotask | スタックが空になった直後 |
| タスク (マクロタスク) | setTimeout, I/O, DOM イベント | マイクロタスクを空にした後 |