JavaScriptはなぜ「待てる」のか — イベントループ
JavaScriptはシングルスレッドです。一度に1つのことしかできません。それなのに、タイマーを待ちながら、通信の返事を待ちながら、画面は固まらずに動き続けます。この「待てる」仕組みの正体がイベントループです。
下の図1では、4行のコードを1ステップずつ実行しながら、コールスタックと2つのキューがどう動くかを観察できます。その前に1つだけ予想してみてください — A・B・C・Dはどの順番で出力されるでしょうか?
CODE — 実行するコード
1
console.log('A'); 2
setTimeout(() => console.log('B'), 0); 3
Promise.resolve().then(() => console.log('C')); 4
console.log('D'); CONSOLE — 出力
(まだ出力はありません)
CALL STACK — 実行中の仕事
MICROTASKS — マイクロタスク(優先)
TASK QUEUE — タスクキュー
STEP 1 / 9
図1 — イベントループのステップ実行(コールスタックと2つのキュー)
ルールは3つだけ
図1の動きは、たった3つのルールで説明できます。(1) コールスタックにある仕事を上から順に実行する。(2) スタックが空になったら、まずマイクロタスクキューを空になるまで全部実行する。(3) それからタスクキューの先頭を1つだけ取り出して実行する。あとはこの(2)と(3)をぐるぐる繰り返す — このループがイベントループです。
大事なのは、setTimeout(..., 0) の「0ミリ秒」でさえすぐには実行されないことです。コールバックはあくまでキューに並ぶだけで、いま実行中のコードが最後まで終わってスタックが空くのを待ちます。だからこそ、同期コードの途中に割り込まれる心配がない代わりに、重い処理でスタックを長く占領すると、キューの仕事が動けず画面が固まります。
用語ミニ辞書
- コールスタック
- 「いま実行中の仕事」の積み重ね。JSはこれが1本しかない(シングルスレッド)。
- タスクキュー
- setTimeoutやクリックイベントのコールバックが並ぶ行列。1周につき1つだけ実行される。
- マイクロタスク
- Promiseの.thenなどが並ぶ優先行列。タスクキューより先に、毎回空になるまで実行される。
まとめ
JavaScriptが「待てる」のは、待ち仕事を自分で抱え込まず、ブラウザに任せて、できあがったらキューに並んでもらうからでした。出力順が A→D→C→B になる理由も、「同期コードが先、次にマイクロタスク全部、最後にタスク1つ」というループの順番そのものです。async/await も中身はPromiseなので、この図のマイクロタスクの動きが分かっていれば怖くありません。