Node.jsの実行原理は、イベントループを中心に構築されています。以下では、その仕組みと各フェーズの動作について説明します。
実行プロセス
- Node.jsが起動すると、まずイベントループが初期化され、提供された入力スクリプト(同期的なメインモジュールコード)が実行されます。この過程で非同期API呼び出しやタイマー、process.nextTick()などが発生し、その後イベントループ処理が始まります。
- Node.jsのイベントループは6つのフェーズに分かれ、順番に反復して実行されます。それぞれのフェーズに入るたびに、対応するコールバックキューから関数を取り出して実行します。
- 各フェーズ内のマクロタスクが終了すると、process.nextTickとPromiseのマイクロタスクが実行されます。そして次のフェーズに移ります。
フェーズの概要
- タイマーフェーズ: setTimeout()やsetInterval()によって設定されたコールバックを実行します。
- 保留中のコールバック: 次のループイテレーションまで遅延されたI/Oコールバックを実行します。
- アイドル/準備: 内部使用のみ。
- ポーリング: 新しいI/Oイベントを取得し、I/O関連のコールバックを実行します(閉じるコールバックやタイマーコールバック、setImmediateコールバック以外のほぼすべてのコールバック)。特にこの「ポーリング」フェーズが重要です。
- チェック: setImmediate()によるコールバックがここで呼び出されます。
- クローズコールバック: ソケットなどのクローズコールバックがここに含まれます。
イベントループは、I/Oタスクがない場合、ポーリングフェーズで待機状態に入ります。新しいI/Oタスクが挿入されるまで休眠します。また、I/Oタスクがある場合、ポーリングキューが空になった後にsetImmediateキューと期限切れタイマーを確認し、存在すれば現在のフェーズを終了し次のフェーズに進みます。
主要なAPI
// チェックフェーズで実行される非同期関数
setImmediate();
// 現在のイベントループフェーズが終了した後、次のフェーズに入る前に実行される非同期関数
// 技術的にはイベントループの一部ではありません
process.nextTick();
// V8エンジンで実装されたマイクロタスク関数であり、イベントループの一部ではありません
Promise;
setImmediate()とsetTimeout()
両者の実行順序は、呼び出される文脈によって異なります。
ケース1:シンプルな例
// test.js
setTimeout(() => {
console.log('タイムアウト');
}, 0);
setImmediate(() => {
console.log('即時実行');
});
// 実行順序は固定されていません
// 原因:システムスケジューリングに依存(マシンの性能や他のアプリケーションによるリソース競合など)
ケース2:同期コードが長時間実行される場合
setTimeout(() => {
console.log('タイムアウト');
}, 0);
setImmediate(() => {
console.log('即時実行');
});
for (let i = 0; i < 1000000; i++) {
// noop
}
// 実行順序:タイムアウト -> 即時実行
// 原因:同期コードの実行時間がタイマーの時間を超えているため、最初のイベントループでタイマーフェーズが優先される
ケース3:ESモジュールの場合
// test.mjs
setTimeout(() => {
console.log('タイムアウト');
}, 0);
setImmediate(() => {
console.log('即時実行');
});
// 実行順序:即時実行 -> タイムアウト
// 原因:グローバル同期コードやマイクロタスクの実行がタイマーの計時を妨げず、初回のイベントループでタイマーのコールバックがキューに追加される確率が低い
ケース4:I/Oコールバック内での比較
// test.jsまたはtest.mjs
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('タイムアウト');
}, 0);
setImmediate(() => {
console.log('即時実行');
});
});
// 実行順序:即時実行 -> タイムアウト
// 原因:I/Oコールバック時はポーリングフェーズにあるため、次のフェーズであるチェックフェーズでsetImmediateが必ず先に実行される