シングルスレッド+イベントループ
主役はこの3つ
- コールスタック(Call Stack)
- 「いま実行中の関数」が積み上がるスタック
foo()からbar()を呼ぶ →fooの上にbarが積まれる →bar終了 →fooに戻る、という普通のスタック挙動
- タスクキュー(マクロタスクキュー)
- 後で実行する「大きめの仕事」の待ち行列
- 例:
setTimeoutのコールバック、ユーザーイベント(クリックなど)、setInterval,MessageChannelなど
- マイクロタスクキュー
- もっと細かい「すぐ実行したい後処理」の待ち行列
- 例:
Promise.then/catch/finally、queueMicrotask、MutationObserverなど
そして、イベントループ がこの3つを回します:
- タスクキューから1つ「タスク」を取り出して、コールスタック上で実行する
- そのタスクで発生した処理が終わり、コールスタックが空になったら
→ マイクロタスクキューを全部処理しきる(空になるまで) - 必要に応じて DOM の再描画などを行う(ブラウザの場合)
- 次のタスクをタスクキューから取ってきて、1に戻る
この 「タスク→マイクロタスク全部→描画→次のタスク…」 のループが JS の実行モデルの核です。
コールスタックの復習
function c() {
console.log("c");
}
function b() {
c();
}
function a() {
b();
}
a();このときのコールスタックの変化は:
a()呼び出し → stack:[a]a内でb()→ stack:[a, b]b内でc()→ stack:[a, b, c]c終了 →[a, b]b終了 →[a]a終了 →[](空)
スタックが空になった瞬間に、イベントループが「次のタスク or マイクロタスク」を実行してよい状態になります。
タスク(マクロタスク)とは何か
タスク(マクロタスク)は、ざっくり言うと:「1回の大きな仕事(1チャンクのJSコード)」です。
代表的なタスク発生源
ブラウザでは:
- 初回に読み込まれた
<script>ブロック setTimeout(fn, delay)やsetInterval(fn, interval)のコールバック- ユーザーイベント(クリック・キーボード入力など)
postMessageのメッセージイベント- XHR / fetch の
onloadとかは、内部的には(ホストの実装にもよるけど)タスクとしてキューに積まれるイメージ
Node.js では:
- タイマー(
setTimeout,setInterval) - I/O 完了のコールバック
setImmediateなど
重要なのは:
- 各タスクは「コールスタックが空になるまで」走り続ける
→ 長い処理を書くと、その間 UI が固まる - タスクが終わるたびに、マイクロタスクキューがすべて処理される
マイクロタスクとは何か
マイクロタスクは、タスクの中でスケジュールされる「より細かい後処理」です。
代表的なマイクロタスクの発生源
Promise.then(...),.catch(...),.finally(...)queueMicrotask(...)MutationObserverのコールバック
流れ:
- タスク中で
Promise.resolve().then(...)などを呼ぶ - その
.thenのコールバックは すぐには実行されず、マイクロタスクキューに積まれる - 現在のタスク(そのJSコード全体)が終わり、コールスタックが空になった瞬間
- イベントループが、マイクロタスクキューから1つずつ取り出して実行
- マイクロタスクの中でさらにマイクロタスクを追加した場合も、キューが空になるまで続ける
setTimeout と Promise.then の実行順
一番よくある「なんでこうなるの?」問題を、マイクロタスクの観点で整理します。
console.log(1);
setTimeout(() => {
console.log("timeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise");
});
console.log(2);実行結果:
1
2
promise
timeoutなぜこうなるか?
- 最初のタスク(このスクリプト全体)が開始
console.log(1)→1setTimeout(...)→ 「コールバックをタスクキューに積んでね」とブラウザに頼む(実際には一定の遅延の後)Promise.resolve().then(...)→ コールバックを マイクロタスクキュー に積むconsole.log(2)→2
- このスクリプト(最初のタスク)が終わり、コールスタックが空になる
- イベントループ:「まずマイクロタスクキューを全部処理しよう」
promiseコールバック実行 →console.log("promise")
- マイクロタスクキューが空になる
- 次のタスクをタスクキューから取り出す
setTimeoutのコールバックがタスクとして実行される →console.log("timeout")
→ 結果として マイクロタスク(Promise.then)が、タスク(setTimeout)よりも先に実行 されます。
queueMicrotask で明示的にマイクロタスクを使う
queueMicrotask は、「いまのタスクが終わったらすぐ実行したい処理」 を登録するAPIです。
console.log("A");
queueMicrotask(() => {
console.log("microtask");
});
console.log("B");実行結果:
A
B
microtaskPromise.resolve().then(...) とほぼ同じタイミングですが、queueMicrotask は 純粋に「マイクロタスクを積む」ためだけのAPI です。
Promise と queueMicrotask の違い
Promise.resolve().then(fn)も内部的にマイクロタスクを使う- ただし Promise は「状態」や「エラー伝搬」などの仕組みもセット
- 「単に後でちょっと実行したいだけ」であれば
queueMicrotask(fn)が素直
マイクロタスクの「全部処理し終えるまで抜けない」という性質
イベントループのルールとして、「マイクロタスクキューが空になるまで次のタスクに進まない」というのがあります。
ループでキューに積み続けるとどうなるか
queueMicrotask(function repeat() {
console.log("microtask");
queueMicrotask(repeat);
});- 最初の
queueMicrotaskが呼ばれた後、タスクが終了すると repeatがマイクロタスクとして実行される- その中でまた
queueMicrotask(repeat)が呼ばれる - マイクロタスクキューは 常に空にならない
- 結果として、「次のタスク」に進むことができず、描画やイベント処理がフリーズする
→ マイクロタスクは 短くて軽い処理 にとどめる必要がある。
async/await とマイクロタスク
async/await は Promise のシンタックスシュガーなので、await の「続き」はマイクロタスクとして実行されます。
async function main() {
console.log("before");
await Promise.resolve();
console.log("after");
}
console.log("start");
main();
console.log("end");実行順:
start
before
end
afterなぜか:
main()を呼んだ時点で、async functionは Promise を返すawait Promise.resolve()に到達した瞬間、「ここで一旦中断」し、- 「
awaitの 後ろの処理」をマイクロタスクとしてキューに積む」イメージ
- 「
- いま実行中のタスク(このスクリプト)が終わる
- マイクロタスクとして
console.log("after")が動く
これも、Promise.then がマイクロタスクに乗るのと同じ原理です。
ブラウザのレンダリングとの関係
ブラウザのイベントループでは、1フレームごとにおおむね:
- タスクを1つ実行
- マイクロタスクを全部実行
- DOMの変更を反映してレイアウト・ペイント(描画)する
- 次のタスクへ…
という流れになっています。
そのため:
- タスクやマイクロタスクの処理が長時間かかると、描画が遅れる
- → スクロールがカクつく、UI が固まって見える
- 「DOM をちょっと変更して、その結果をすぐに測定したい」ようなときに
Promise.resolve().then(...)/queueMicrotask(...)で「1タスク後」に回すテクニックが使われる
ただし、マイクロタスクで長大な処理や無限ループをすると、
永遠に描画フェーズにたどり着けない ので注意です。
Node.js のマイクロタスク
- Node.js でも Promise の
then/catch/finallyは マイクロタスクキュー に積まれる - Node には
process.nextTickという「マイクロタスクよりさらに優先度が高い」キューもある- あまり多用するとイベントループが回らなくなるので注意、という点はマイクロタスクと似ている
「Node も基本は同じ:タスク処理の合間にマイクロタスク(Promiseなど)が挟まる」
というくらいを押さえておけば十分です。
コメント