概要
JavaScriptの非同期処理は、イベントループ(Event Loop)という実行モデルの上で動いている。
この仕組みを正しく理解することで、以下のような非同期設計が可能になる:
- なぜ
Promise
の.then()
はsetTimeout
より先に呼ばれるのか? - UIが固まる処理を、どうすれば回避できるのか?
- ループ内の非同期処理は、なぜ意図通りに動かないのか?
この記事では、イベントループの構造・マイクロタスクとマクロタスクの違い・UIと非同期の干渉などを整理し、非同期処理の裏側を設計視点で読み解く。
イベントループの基本構造
[Call Stack] ← 同期処理
[Task Queue] ← マクロタスク:setTimeout, setInterval, I/O
[Microtask Queue] ← マイクロタスク:Promise, queueMicrotask
Event Loop:
1. Call Stack が空になるのを待つ
2. Microtask Queue を全て実行
3. 次に Task Queue の先頭を 1 件実行
4. 繰り返す
タスクの種類
タイプ | 実行タイミング | 代表API |
---|---|---|
マクロタスク | 各イベントループの最後 |
setTimeout , setInterval , setImmediate , MessageChannel , I/O |
マイクロタスク | 各イベントループの中間 |
Promise.then , catch , finally , queueMicrotask
|
実行順の違い:コードで検証
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
出力順:
sync
promise
timeout
✅ 理由
-
console.log('sync')
: Call Stack 上の同期処理 -
.then(...)
: Microtask Queue に追加 -
setTimeout
: Task Queue に追加 - Event Loop は、同期 → Microtask → Task の順で実行
Microtask の優先度
Promise.resolve().then(() => {
console.log('first');
Promise.resolve().then(() => console.log('second'));
});
出力順:
first
second
→ ✅ Microtask はEvent Loop内で完全に消化されるまで続く
queueMicrotask
の使いどころ
queueMicrotask(() => {
console.log('executed last (but before setTimeout)');
});
→ ✅ UIをブロックせず、“でも極力早く”実行したいときに使える
UIとイベントループ:ブロッキング設計を避ける
❌ 悪い例(同期処理でUIを固める)
document.getElementById('btn').addEventListener('click', () => {
// 長い処理
for (let i = 0; i < 1e9; i++) {}
alert('done');
});
→ ✅ イベント処理中は Call Stack が詰まるため、UI更新も遅延する
✅ 解決策:非同期化して分割実行
document.getElementById('btn').addEventListener('click', () => {
setTimeout(() => {
for (let i = 0; i < 1e9; i++) {}
alert('done');
}, 0);
});
設計におけるベストプラクティス
目的 | 推奨戦略 |
---|---|
優先度の高い非同期処理 |
Promise.then / queueMicrotask
|
長い処理でUIを固めないようにしたい |
setTimeout(..., 0) で分割 |
イベント後に直ちに実行(でも非同期) | queueMicrotask() |
複数非同期ステップを順序制御したい |
async/await + Promise
|
よくある誤解
❌ setTimeout(..., 0)
は“すぐに実行”される?
→ ❌ 違います。“最速でも1ループ後”
→ ✅ Microtask (Promise
) の方が先に処理される
❌ Promise の中に setTimeout を入れても Microtask にはならない
new Promise(resolve => {
setTimeout(() => {
resolve('done');
}, 0);
});
→ setTimeout
はマクロタスク。Promise内でも Microtask にはならない
結語
JavaScriptの非同期処理は、構文ではなく“タイミングの設計”である。
その制御の基盤となるのが、イベントループとタスクキューである。
- 同期 → Microtask → Macrotask の流れを理解することで、
- 非同期の「なぜ今動かない?」を制御できるようになり、
- UIとロジックの衝突を避けた洗練された設計が可能になる
構文を知っていても、タイミングを知らなければ設計はできない。
イベントループとは、「時間の構文」である。