JavaScript イベント ループ: 実際の仕組み、コール スタック、マイクロタスク、マクロタスク
イベント ループは JavaScript の核心ですが、ほとんどの開発者はそれを理解していません 曖昧な。コールスタック、Web API、コールバックキュー、マイクロタスクキュー(Promise)、 マクロタスク キュー (setTimeout) と、その実行順序に驚かれる理由について説明します。
JavaScript はシングルスレッドです: その本当の意味
JavaScriptには、 単一の実行スレッド: いつでも 1 つだけ JavaScript ステートメントが実行されています。これは実行時の事実であり、 言語の限界。 JavaScript エンジン (Chrome/Node.js では V8、Firefox では SpiderMonkey) コードを順番に実行します。
それでも、Node.js は数万の同時リクエストを処理します。として?を介して
イベントループ: 非同期 I/O 操作を待機できる機構
(ネットワーク、ファイルシステム) スレッドをブロックせずに、実際の作業をオペレーティング システムに委任します。
経由 libuv.
イベントループのコンポーネント
Node.js (V8 + libuv) のイベント ループは次のもので構成されます。
- コールスタック: JavaScript コードの同期実行スタック。いつ 関数が呼び出されると、その関数はスタックに「プッシュ」されます。終了すると「供給」されます。 スタックが空でない場合、スレッドはビジー状態です。
-
Web API / ノード API: 環境 (ブラウザまたは Node.js) によって提供されるインターフェース
非同期 I/O 操作の場合:
setTimeout,fetch,fs.readFile、 WebSocket など。これらの操作は C++ ランタイム (libuv) に委任され、JS スレッドは使用されません。 - コールバックキュー (マクロタスクキュー): 次に実行するコールバックのキュー I/O 操作の完了、タイムアウト、間隔 コールスタックが空になった後に実行 そしてマイクロタスクキューは空になります。
-
マイクロタスクキュー: Promise コールバックの高優先キュー
(
.then(),async/await) そしてqueueMicrotask()。彼が来ます 各マクロタスクの前に完全に空になります。
実行順序: 基本的なルール
イベントループの最も重要なルール:
- コールスタック上のすべての同期コードを実行します。
- マイクロタスクキューを完全にクリアします (Promise、MutationObserver)
- を実行します。 次のマクロタスク コールバック キューから (setTimeout、I/O コールバック)
- ポイント 2 に戻る
これは、多くの開発者を驚かせる動作の説明です。
console.log('1 - sincrono');
setTimeout(() => console.log('4 - macrotask'), 0);
Promise.resolve()
.then(() => console.log('2 - microtask 1'))
.then(() => console.log('3 - microtask 2'));
console.log('1b - sincrono');
// Output:
// 1 - sincrono
// 1b - sincrono
// 2 - microtask 1
// 3 - microtask 2
// 4 - macrotask
// Perché? Il setTimeout con delay=0 è comunque un macrotask.
// Le Promise sono microtask, eseguite PRIMA del prossimo macrotask.
イベント ループを段階的に表示する
// Esempio completo — traccia mentale dell'esecuzione
async function main() {
console.log('A'); // [1] push main, push log('A')
setTimeout(() => console.log('B'), 0); // [2] delega a Web APIs
await Promise.resolve('resolved'); // [3] sospende main, schedula microtask
console.log('C'); // [6] riprende dopo microtask
}
console.log('D'); // [4] esegue sincrono
main(); // [1] chiama main
console.log('E'); // [5] esegue sincrono dopo main si sospende
// Esecuzione:
// [1] D (sincrono prima di main)
// [2] A (main inizia, prima console.log)
// [3] main si sospende su await
// [4] E (codice sincrono dopo main())
// [5] Stack vuoto, microtask queue: Promise.resolve callback
// [6] C (main riprende dopo await)
// [7] Stack vuoto, macrotask queue: setTimeout callback
// [8] B
// Output finale: D, A, E, C, B
Promise と async/await: 内部
async/await それは Promise の上にある糖衣構文です。コンパイラは変換します
ごとに await in un .then()。変換を理解することは理解に役立ちます
実行順序:
// Questa funzione async:
async function processUser(id) {
const user = await fetchUser(id); // await punto 1
const orders = await fetchOrders(user); // await punto 2
return { user, orders };
}
// È equivalente a:
function processUserWithPromises(id) {
return fetchUser(id)
.then(user => {
return fetchOrders(user)
.then(orders => { return { user, orders }; });
});
}
// La funzione si "sospende" a ogni await e
// riprende quando la Promise risolve (tramite microtask queue)
マイクロタスクの飢餓: 避けるべきアンチパターン
マイクロタスク キューが空にならない場合、マクロタスク (したがって I/O コールバック) は空になりません。 決して実行されませんでした。これはと呼ばれます マイクロタスク飢餓:
// ANTI-PATTERN: loop infinito di microtask — blocca tutto!
function recursiveMicrotask() {
Promise.resolve().then(recursiveMicrotask); // schedula infiniti microtask
}
recursiveMicrotask();
// setTimeout di seguito non verrà MAI eseguito!
// PATTERN CORRETTO: usa setImmediate (Node.js) o setTimeout per cedere il controllo
function processLargeDataset(data, index = 0) {
if (index >= data.length) return;
processItem(data[index]);
// Cede il controllo all'event loop ogni 100 elementi
if (index % 100 === 0) {
setImmediate(() => processLargeDataset(data, index + 1));
} else {
processLargeDataset(data, index + 1);
}
}
Node.js イベント ループ: フェーズ
Node.js イベント ループには、ブラウザーを超えた特定のステージ (libuv) があります。
// Le fasi del Node.js event loop (semplificato)
// 1. timers: esegue callback di setTimeout e setInterval
// 2. I/O callbacks: callback I/O differite (errori socket)
// 3. idle, prepare: uso interno Node.js
// 4. poll: recupera I/O events, esegue callback I/O
// 5. check: esegue callback di setImmediate()
// 6. close callbacks: es. socket.on('close', callback)
// setImmediate vs setTimeout(fn, 0): NON garantiti nell'ordine
// dipende dal momento di chiamata nel ciclo
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Output non deterministico se eseguiti nel main module
// Ma dentro una callback I/O:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Qui 'immediate' è sempre PRIMA — siamo già nella fase poll
});
イベント ループのブロック: 典型的な間違い
イベントループをブロックする操作
- CPU を大量に使用する同期コンピューティング: 2 秒間のブロックを計算するループ 他のすべてのリクエストは 2 秒間行われます。 CPU バウンドの作業にはワーカー スレッドを使用します。
- 巨大なペイロードに対する JSON.parse(): 10MB JSON の解析は 同期とブロック。 JSON ストリームを使用するか、ワーカー スレッドに委任します。
- サーバーコードの fs.readFileSync(): すべての Node.js API のバージョンを同期します 彼らはスレッドをブロックします。常にコールバックまたは待機のある非同期バージョンを使用してください。
-
フラグを使用しない暗号化操作: 暗号化操作は CPU に依存します。
crypto.scrypt()コールバックを受け入れる。crypto.scryptSync()ブロック。
結論
イベント ループは、シングルスレッドであるにもかかわらず JavaScript を効率的にするものです。
覚えておくべき重要な点: マイクロタスク キューはマクロタスク キューよりも優先されます。 await
スレッドはブロックされませんが、現在の関数は一時停止されます。 CPU バウンドの同期操作ブロック
イベントループ全体。
次の記事では、 goroutine と Go チャネル:モデル CSP に基づいており、数百万のタスクに拡張できるように設計された、まったく異なる同時実行プラットフォーム 競合他社。







