Node.jsは非同期処理が得意なプラットフォームとして知られていますが、その中核を担うのが「イベントループ(Event Loop)」です。非同期APIの処理順序を理解することで、意図しない遅延やバグを防ぐことができます。
本記事では、特に混乱しやすいsetTimeout
とPromise
(マイクロタスク)の優先度の違いを通じて、Node.jsにおけるイベントループの挙動を図解と例付きで解説します。
イベントループとは何か
Node.jsでは、シングルスレッドの中で非同期I/Oを効率的にさばくために「イベントループ」というメカニズムを採用しています。イベントループは、キューに入ったタスクを順番に処理し、完了したら次のループへと進みます。
タスクは大きく分けて以下の2種類に分類されます。
- マクロタスク(Macrotask):setTimeout, setInterval, setImmediate など
- マイクロタスク(Microtask):Promise.then, queueMicrotask など
イベントループの1サイクルで、マクロタスク1つを処理 → その後、すべてのマイクロタスクを処理という流れになります。
setTimeoutとPromiseの優先度を比較
以下のコードを実行してみましょう。
console.log('開始');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('終了');
このコードの出力結果は以下のようになります:
開始
終了
Promise
setTimeout
この順番になる理由は、Promise
はマイクロタスクキューに追加され、setTimeout
はマクロタスクキューに追加されるためです。
イベントループの流れに沿って解説すると:
- まず同期処理(
console.log('開始')
とconsole.log('終了')
)が実行 - 次にマイクロタスク(
Promise.then()
)が全て実行 - その後、マクロタスク(
setTimeout
)が処理される
より複雑な例
setTimeout(() => {
console.log('タイマー1');
Promise.resolve().then(() => {
console.log('タイマー1の中のPromise');
});
}, 0);
Promise.resolve().then(() => {
console.log('メインのPromise');
});
setTimeout(() => {
console.log('タイマー2');
}, 0);
このコードの出力結果は:
メインのPromise
タイマー1
タイマー1の中のPromise
タイマー2
それぞれのタイミングで追加されたマクロ・マイクロタスクの優先度によって、順序が決まっていることが分かります。
Node.jsにおけるsetImmediateとの違い
setImmediate
はNode.js特有の関数で、setTimeout(..., 0)
よりも「次のイベントループの直後」に実行されるという違いがあります。
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
この出力順は環境によって前後することがありますが、内部的にはsetImmediateの方がマクロタスクよりも早く処理される可能性が高いです。
まとめ|非同期の理解はイベントループから
Node.jsで非同期処理を書く際には、PromiseはsetTimeoutよりも先に実行されるという基本を押さえておくことが重要です。
複雑なアプリケーションになるほど、非同期の順序や実行タイミングはバグや意図しない挙動の原因になります。イベントループの仕組みと各種タスクの優先順位を正しく理解し、コードの可読性と安定性を高めましょう。