Promise、then、setTimeoutについて理解があいまいだったのでまとめてみました。
thenはいつ実行される?
いきなりですが、以下のコードの実行順序について考えてみます。
console.log("A");
new Promise((resolve, reject) => {
resolve();
console.log("B");
}).then(() => console.log("C"));
console.log("D");
上記を実行するとA
→B
→D
→C
の順で出力されます。
まずA
出力され、次にPromiseのコンストラクタに渡された関数内のresolve()が実行され、B
が出力されます。(Promiseは非同期処理を扱うためのものですが、Promiseのコンストラクタに渡された関数は同期的に実行されます)そして、コンストラクタから返されたfulfilledの状態のPromiseオブジェクトで.then
が呼ばれます。すでにresolve()が実行されているので、C
がすぐに出力されそうですが、実際はD
の後に出力されます。なぜすぐに実行されないのでしょうか?それにはJavaScriptの非同期処理について理解する必要があります。
JavaScriptの非同期処理
JavaScriptは基本的にシングルスレッドで実行さるので、処理が同時に実行されることはありません。同時に実行されているように見えるのは、イベントループにより実行を遅らせたり分割したりするからです。イベントループとは以下のような繰り返しです。
- キューからタスクを取り出す(なければ待機)
- 実行する
キューには2種類ありタスクキューとマイクロタスクキューがあります。setTimoutを使うと処理はタスクキューに追加されます。Promiseのthenを使うと処理はマイクロタスクキューに追加されます。実行される順番は以下のようになります。
- タスクキューからタスクを取り出し実行する
- マイクロタスクキューが空になるまでマイクロタスクを実行する(その間新しく追加されたとしても)
- 必要であればレンダリングする
非同期処理の例
もう一度先ほどのコードに戻ります。
console.log("A");
new Promise((resolve, reject) => {
resolve();
console.log("B");
}).then(() => console.log("C")); // マイクロタスキューに追加される
console.log("D");
まず、プログラムがタスクとして読み込まれ、同期的にA
→B
→D
に出力されます。最後に非同期でC
が出力されるのは、thenの処理はマイクロタスクとして現在のタスクが完了した後に実行されるからです。
Promiseの部分は以下のように書き換えることができます。
Promise.resolve().then(() => console.log("Microtask"));
そして上記のコードはマイクロタスキューに追加しているだけなので、さらに以下のように書き換えることができます。
queueMicrotask(() => console.log("Microtask"));
queueMicrotask()
はsetTimeout()
のようなタスクよりも先に実行されます。
実行順序は以下の通りです。
console.log("A");
setTimeout(() => console.log("Task"));
queueMicrotask(() => console.log("Microtask1"));
queueMicrotask(() => {
console.log("Microtask2");
queueMicrotask(() => console.log("Microtask3"));
});
console.log("B");
A
B
Microtask1
Microtask2
Microtask3
Task
まず、同期的にA
とB
が出力されます。そしてレンダリングや次のタスクの前にマイクロタスクが実行されるのでMicrotask1
とMicrotask2
が出力されます。その後マイクロタスク内でさらにマイクロタスクを追加しています。途中で追加されたマイクロタスクも実行されMicrotask3
と出力されます。差後にsetTimeoutでタスキューに追加されたタスクが実行されTask
と出力されます。この順番はマイクロタスクの処理にどれほど時間がかかったとしても変わりません。マイクロタスクが追加され続けると画面の更新処理が行えず、画面が固まってしまうので気を付ける必要があります。