【保存版】JavaScript のイベントループとタスクキューを図解で理解する

JavaScript を使って開発していると、「あれ?なんでこのコードの実行順序が思った通りにならないんだろう?」と疑問に思ったことはありませんか?特に setTimeout や Promise を使った非同期処理で、予想と違う実行結果に戸惑った経験がある方も多いでしょう。
実は、この現象の背景には「イベントループ」と「タスクキュー」という JavaScript の核心的な仕組みが関わっています。これらを理解することで、非同期処理の動作を正確に予測できるようになり、より効率的でバグの少ないコードを書けるようになりますね。
背景
JavaScript のシングルスレッドの特性
JavaScript は シングルスレッド で動作する言語です。これは、一度に一つの処理しか実行できないことを意味します。
javascript// 同期処理の例
console.log('処理1'); // 最初に実行
console.log('処理2'); // 次に実行
console.log('処理3'); // 最後に実行
上記のコードは順番通りに実行され、「処理 1」「処理 2」「処理 3」の順で出力されますね。しかし、実際の Web アプリケーションでは、API 呼び出しやユーザーのクリックイベント、タイマー処理など、様々な非同期処理を扱う必要があります。
非同期処理が必要な理由
シングルスレッドの JavaScript で同期処理のみを使った場合、以下のような問題が発生します。
javascript// 時間のかかる同期処理の例
function heavyCalculation() {
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}
console.log('開始');
heavyCalculation(); // この処理中はブラウザが固まる
console.log('終了');
この例では、重い計算処理が実行されている間、ブラウザの UI が完全に固まってしまいます。ユーザーはボタンをクリックすることも、スクロールすることもできなくなってしまうんです。
非同期処理を使うことで、このような問題を解決できます。
javascript// 非同期処理の例
console.log('開始');
setTimeout(() => {
console.log('非同期処理完了');
}, 0);
console.log('終了');
ブラウザと Node.js での違い
イベントループの基本的な仕組みは同じですが、実行環境によって細かな違いがあります。
ブラウザ環境での特徴:
- DOM イベント処理
- Web APIs(fetch、setTimeout など)
- レンダリングと連携した処理
Node.js 環境での特徴:
- ファイルシステム操作
- ネットワーク処理
- より詳細なタスクキューの分類
どちらの環境でも、イベントループの基本概念を理解していれば、適切に対応できますよ。
課題
同期処理の限界
同期処理だけでは、現代の Web アプリケーションに求められる要件を満たすことができません。
javascript// 問題のあるコード例
function fetchUserData() {
// この処理に3秒かかると仮定
const userData = syncApiCall('/api/user');
return userData;
}
// UI更新処理
function updateUI() {
console.log('UIを更新中...');
}
fetchUserData(); // 3秒間ブロック
updateUI(); // 3秒後にようやく実行
この例では、API 呼び出しが完了するまで UI の更新が一切できず、ユーザー体験が大幅に悪化してしまいます。
コールバック地獄の問題
非同期処理を扱う初期の方法として「コールバック関数」がありましたが、複雑な処理になると可読性が著しく低下します。
javascript// コールバック地獄の例
getData(function (a) {
getMoreData(a, function (b) {
getEvenMoreData(b, function (c) {
getFinalData(c, function (d) {
// やっと処理完了
console.log('最終結果:', d);
});
});
});
});
このような「コールバック地獄」は、コードの保守性を大幅に損ねてしまいますね。
パフォーマンスのボトルネック
イベントループの仕組みを理解せずにコードを書くと、意図しないパフォーマンスの問題が発生することがあります。
javascript// パフォーマンス問題のある例
for (let i = 0; i < 1000; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
この例では、1000 個のタイマーが作成され、すべてタスクキューに積まれます。メモリ使用量の増加や処理の遅延につながる可能性があるんです。
解決策
イベントループの仕組み
以下の図は、JavaScript のイベントループの基本的な構造を示しています。
mermaidflowchart LR
code[JavaScript コード] -->|関数呼び出し| stack[コールスタック]
stack -->|Web API 呼び出し| api[Web APIs]
api -->|コールバック| queue[タスクキュー]
queue -->|イベントループ| stack
subgraph browser[ブラウザ環境]
api
queue
loop[イベントループ]
end
loop -.->|監視| stack
loop -.->|監視| queue
イベントループは、コールスタックが空になったときにタスクキューから次の処理を取り出して実行する役割を担っています。
JavaScript エンジンの主要コンポーネントは以下の通りです:
1. コールスタック(Call Stack) 現在実行中の関数を管理するスタック構造です。
javascriptfunction first() {
console.log('first');
second();
}
function second() {
console.log('second');
third();
}
function third() {
console.log('third');
}
first(); // コールスタック: third -> second -> first
2. ヒープメモリ(Heap) オブジェクトや変数が格納されるメモリ領域です。
3. Web APIs ブラウザが提供する非同期機能(setTimeout、fetch、DOM イベントなど)です。
タスクキューの種類と優先順位
JavaScript には複数のタスクキューが存在し、それぞれ異なる優先順位を持っています。
mermaidflowchart TD
start[コールスタック空?] -->|Yes| micro[マイクロタスクキュー確認]
micro -->|あり| exec1[マイクロタスク実行]
exec1 --> micro
micro -->|なし| macro[マクロタスクキュー確認]
macro -->|あり| exec2[マクロタスク実行]
exec2 --> start
macro -->|なし| render[レンダリング処理]
render --> start
start -->|No| wait[待機]
wait --> start
優先順位(高い順):
順位 | タスクキューの種類 | 例 |
---|---|---|
1 | マイクロタスクキュー | Promise.then()、queueMicrotask() |
2 | マクロタスクキュー | setTimeout()、setInterval()、I/O 処理 |
3 | レンダリング | requestAnimationFrame() |
マイクロタスクとマクロタスクの違い
この優先順位の違いを理解することが、非同期処理を正確に予測する鍵となります。
マイクロタスクの特徴:
- Promise の then/catch/finally
- async/await の完了時
- queueMicrotask()
javascript// マイクロタスクの例
Promise.resolve().then(() => {
console.log('マイクロタスク1');
});
Promise.resolve().then(() => {
console.log('マイクロタスク2');
});
マクロタスクの特徴:
- setTimeout/setInterval
- DOM イベント
- I/O 処理
javascript// マクロタスクの例
setTimeout(() => {
console.log('マクロタスク1');
}, 0);
setTimeout(() => {
console.log('マクロタスク2');
}, 0);
具体例
setTimeout と Promise の実行順序
実際のコード例で、イベントループの動作を確認してみましょう。
javascriptconsole.log('1: 同期処理開始');
setTimeout(() => {
console.log('4: マクロタスク(setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3: マイクロタスク(Promise)');
});
console.log('2: 同期処理終了');
実行順序の詳細解説:
console.log('1: 同期処理開始')
- 即座に実行setTimeout()
- Web API に登録、コールバックはマクロタスクキューへPromise.resolve().then()
- コールバックはマイクロタスクキューへconsole.log('2: 同期処理終了')
- 即座に実行- コールスタックが空になる
- マイクロタスクキューを確認 →
console.log('3: マイクロタスク(Promise)')
実行 - マクロタスクキューを確認 →
console.log('4: マクロタスク(setTimeout)')
実行
async/await の内部動作
async/await は Promise の syntactic sugar ですが、その内部動作を理解することが重要です。
javascriptasync function example() {
console.log('1: async関数開始');
await Promise.resolve();
console.log('3: await後の処理');
return '完了';
}
console.log('0: 処理開始');
example().then((result) => {
console.log('4: async関数の戻り値:', result);
});
console.log('2: async関数呼び出し後');
実行フローの解説:
javascript// await は以下のように変換されます
function example() {
console.log('1: async関数開始');
return Promise.resolve().then(() => {
console.log('3: await後の処理');
return '完了';
});
}
await は内部的に Promise の then() と同じ動作をするため、マイクロタスクキューに登録されるんです。
DOM イベントとタスクキューの関係
DOM イベントもマクロタスクキューを通じて処理されます。
javascript// HTML要素の取得
const button = document.getElementById('myButton');
// イベントリスナーの設定
button.addEventListener('click', () => {
console.log('2: ボタンクリック処理');
Promise.resolve().then(() => {
console.log('3: クリック内のマイクロタスク');
});
});
// 同期的な処理
console.log('1: イベント設定完了');
// プログラムからクリックを発火
button.click();
console.log('4: プログラム処理継続');
実際のコード例とステップ実行
より複雑な例で、イベントループの理解を深めてみましょう。
javascriptconsole.log('=== 開始 ===');
setTimeout(() => console.log('タイマー1'), 0);
Promise.resolve()
.then(() => console.log('Promise1'))
.then(() => console.log('Promise2'));
setTimeout(() => console.log('タイマー2'), 0);
Promise.resolve().then(() => {
console.log('Promise3');
setTimeout(() => console.log('Promise内のタイマー'), 0);
});
console.log('=== 終了 ===');
予想される出力順序:
diff=== 開始 ===
=== 終了 ===
Promise1
Promise2
Promise3
タイマー1
タイマー2
Promise内のタイマー
ステップバイステップの実行過程:
ステップ | 処理内容 | コールスタック | マイクロタスク | マクロタスク |
---|---|---|---|---|
1 | console.log('=== 開始 ===') 実行 | empty | empty | empty |
2 | setTimeout() 登録 | empty | empty | [タイマー 1] |
3 | Promise.resolve().then() 登録 | empty | [Promise1] | [タイマー 1] |
4 | 2 個目のsetTimeout() 登録 | empty | [Promise1] | [タイマー 1, タイマー 2] |
5 | 2 個目のPromise.resolve().then() 登録 | empty | [Promise1, Promise3] | [タイマー 1, タイマー 2] |
6 | console.log('=== 終了 ===') 実行 | empty | [Promise1, Promise3] | [タイマー 1, タイマー 2] |
まとめ
イベントループ理解のポイント
JavaScript のイベントループとタスクキューを理解する上で重要なポイントをまとめます。
1. 実行順序の優先順位
- 同期処理 → マイクロタスク → マクロタスク の順で実行される
- マイクロタスクは完全に空になるまで連続実行される
- マクロタスクは 1 つずつ実行される
2. 非同期処理の分類
javascript// マイクロタスク(高優先度)
Promise.resolve().then(() => {});
async function example() {
await Promise.resolve();
}
queueMicrotask(() => {});
// マクロタスク(低優先度)
setTimeout(() => {}, 0);
setInterval(() => {}, 1000);
setImmediate(() => {}); // Node.js のみ
3. デバッグ時の確認方法
- ブラウザの開発者ツールで実行順序を確認
- console.log で処理の流れを可視化
- Performance タブでタスクの実行時間を測定
実際の開発での活用方法
パフォーマンス最適化:
javascript// 悪い例:UIをブロックする処理
function processLargeData(data) {
data.forEach((item) => {
// 重い処理
processItem(item);
});
}
// 良い例:処理を分割してUIをブロックしない
function processLargeDataAsync(data) {
const batchSize = 100;
let index = 0;
function processBatch() {
const endIndex = Math.min(
index + batchSize,
data.length
);
for (let i = index; i < endIndex; i++) {
processItem(data[i]);
}
index = endIndex;
if (index < data.length) {
setTimeout(processBatch, 0); // 次のバッチを非同期で処理
}
}
processBatch();
}
エラーハンドリングの改善:
javascript// Promise チェーンでのエラーハンドリング
fetchUserData()
.then((userData) => processUserData(userData))
.then((processedData) => updateUI(processedData))
.catch((error) => {
console.error('処理エラー:', error);
showErrorMessage();
});
テストでの活用:
javascript// 非同期処理のテスト
async function testAsyncFunction() {
const result = await myAsyncFunction();
// マイクロタスクの完了を待つ
await new Promise((resolve) => setTimeout(resolve, 0));
expect(result).toBe(expectedValue);
}
これらの知識を活用することで、より効率的で予測可能な JavaScript コードを書けるようになります。イベントループの理解は、現代の Web 開発において必須のスキルと言えるでしょう。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来