イベントループ — JavaScriptが非同期処理を行う仕組み
このトピックを終えると
JavaScriptがシングルスレッドでありながら、非同期処理をどのように行うかの仕組みを理解できるようになります。
シングルスレッドなのに、どうやって?
JavaScriptは一度に一つのタスクしか実行しません。つまり、シングルスレッドです。しかし、タイマー、ネットワークリクエスト、クリックイベントなどを同時に処理します。どのように?
その秘密は**イベントループ(Event Loop)**にあります。JavaScriptエンジン(V8)が単独で処理するわけではありません。ブラウザ(またはNode.js)がサポートしています。
3つの構成要素
コールスタック(Call Stack): 現在実行中の関数が積み重ねられる場所です。関数を呼び出すと積み重ねられ、実行が終了すると取り除かれます。LIFO(Last In, First Out)構造です。
Web API: ブラウザが提供する機能です。setTimeout、fetch、DOMイベントリスナーなどがあります。JavaScriptエンジン外で動作します。
タスクキュー(Task Queue): Web APIで完了したコールバックが待機するキューです。コールスタックが空になると、ここから一つずつ取り出して実行します。
実行順序
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");出力: 1, 3, 2.
0msのタイマーなのに、「2」が最後に表示されます。流れを追ってみましょう。
console.log("1")→ コールスタックで実行 → 出力setTimeout→ Web APIにタイマーを登録 → コールスタックから取り除かれるconsole.log("3")→ コールスタックで実行 → 出力- コールスタックが空 → イベントループがタスクキューを確認 → コールバックを実行 → 「2」が出力
setTimeout(fn, 0)は、「今すぐ」ではなく、「コールスタックが空になったときに」という意味です。
マイクロタスクキュー
Promiseの.then()は、タスクキューではなく、**マイクロタスクキュー(Microtask Queue)**に入ります。マイクロタスクは、タスクよりも先に実行されます。
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");出力: 1, 4, 3, 2.
コールスタックが空になると、イベントループは次の処理を行います。
- まず、マイクロタスクキューをすべて空にします(Promiseコールバック)。
- 次に、タスクキューから1つ取り出して実行します(setTimeoutコールバック)。
async/awaitも内部的にPromiseを使用するため、マイクロタスクです。
コールスタックを長時間占有すると
while (true) {
// 無限ループ
}コールスタックが空にならないと、イベントループが動作しません。クリックしても反応がなく、タイマーも実行されません。ブラウザは「このページが応答していません」と表示します。
重い計算を行う必要がある場合は、Web Workerを使用するか、タスクを分割してコールスタックを定期的に空にする必要があります。
重要なポイント
JavaScriptはシングルスレッドですが、ブラウザのWeb APIとイベントループによって非同期処理を行います。
setTimeout(fn, 0)は、「コールスタックが空になったら実行」という意味です。 **マイクロタスク(Promise)**は、タスク(setTimeout)よりも常に先に実行されます。