LinkedInを見ていたら以下のような場合、どういう順番でログが出力されるのかについて問題を出しているポストがあった。
普段はvueを使っているのでvueのライフサイクルはある程度わかっているつもりだったが、JavaScriptのライフサイクルは全然わからなかった。
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
Promise.resolve().then(() => {
console.log("C");
}).then(() => {
console.log("D");
});
console.log("E");
答えから先に言うと、上の場合はAECDB
の順番で出力される。
jsのライフサイクル
まず、jsのイベントループを理解するにあたって、コールスタック
という概念が核になる。
コールスタックとは、プログラムの実行順序を管理するためのデータ構造である。
スタックは「後入先出」(LIFO: Last In, First Out)の原則で動作し、最後に追加された処理が最初に実行される。
コールスタックは、関数が呼び出されるたびに、その関数の情報をスタックの一番上に追加しする。関数の実行が完了すると、その情報はスタックから取り除かれていく。
タスクの種類
そこで、jsでは非同期処理が可能であるため、タスクごとに実行されるタイミングを調整する必要がある。そのため、タスクは以下のような種類で分けられる。
- 同期タスク:即時実行
- マイクロタスク:
Promise
、queueMicrotask()
、MutationObserver
等 - マクロタスク:
setTimeout
、setInterval
、requestAnimationFrame
、I/O操作
等
実行順序
同期タスク → マイクロタスク → マクロタスク
実はコールスタック以外にもjsにはコールバックキューとマイクロキューというものがある。
コールバックキュー(タスクキューとも呼ばれる) = マクロタスク用
マイクロキュー = マイクロタスク用
だと理解するとわかりやすい。
以下のような流れで順番通りに実行されるようになっている。
- スクリプトを読み込む
- 同期タスクであれば実行する
- マクロタスクだったらコールバックキューに、マイクロだったらマイクロキューに入れる(実行はしない)
- スクリプトが終わるとマイクロキューのタスクを実行する
- マイクロキューが終わるとコールバックキューを実行する
具体例
ということで、自分なりにタスクを並べて、実行される順番を考えてみた。
console.log('1. スクリプトスタート');
setTimeout(() => console.log('2. setTimeout 0'), 0);
Promise.resolve().then(() => console.log('3. Promise.resolve'));
queueMicrotask(() => console.log('4. queueMicrotask'));
Promise.resolve().then(() => {
console.log('5. Nested Promise.resolve');
setTimeout(() => console.log('6. Nested setTimeout'), 0);
});
async function asyncFunction() {
console.log('7. async関数内');
await Promise.resolve();
console.log('8. await後');
}
asyncFunction();
requestAnimationFrame(() => console.log('9. requestAnimationFrame'));
console.log('10. スクリプトおわり');
// 出力順番:
// 同期タスク
// 1. スクリプトスタート
// 10. スクリプトおわり
// マイクロタスク
// 3. Promise.resolve
// 4. queueMicrotask
// 5. Nested Promise.resolve
// 7. async関数内
// 8. await後
// マクロタスク
// 2. setTimeout 0
// 6. Nested setTimeout
// 9. requestAnimationFrame
おわりに
vueのライフサイクルよりこっちを先に勉強するべきだった。
今まで非同期処理がどうなっていたかも知らずにやってきたとは...