はじめに
JavaScriptにおけるイベントループについて学び直しました。
イベントループとは何か理解する上で、JavaScriptの特性について解説し順を追って解説していきます。
JavaScriptのシングルスレッドとイベントループ
JavaScriptは非同期操作を行う能力を持つシングルスレッドのプログラミング言語です。
まずはシングルスレッドとは何かから理解していきましょう。
シングルスレッド
シングルスレッドとは、一度に一つのタスクしか処理できない状態を指します。これはJavaScriptの主要な特性です。
しかし、ネットワークリクエストのような待ち時間の長い操作や、タイムアウトのような遅延操作を行う際には、その処理が完了するまで他のすべての処理がブロックされてしまうという問題が発生します。
ここで非同期処理の出番です。
非同期処理
非同期処理は、待ち時間の長いタスクをバックグラウンドで実行し、その完了後に結果を処理する方法です。この処理が行われる間、JavaScriptは他のタスクを実行することができます。これにより、ユーザーはウェブページを自由に操作したり、他のタスクが実行されるのを待つことなく、別の処理を実行出来ます。
しかし、これらのバックグラウンドに回った非同期タスクはどのようにメインスレッドに戻り実行されるのでしょうか?
ここでイベントループが登場します。
イベントループ
イベントループは、非同期操作を扱うためのJavaScriptの内部メカニズムの一部です。つまり、JavaScriptの処理が実行される仕組みという意味です。
イベントループは以下の順に実行されます。
-
タスクキュー:イベントループはタスクキュー(またはメッセージキュー)と呼ばれるキューからタスクを取得します。
-
タスクの実行:取得されたタスク(関数)が呼び出され、その実行が完了するまでイベントループは次のタスクに進みません。
-
次のタスク:タスクの実行が完了すると、イベントループはタスクキューから次のタスクを取得します。そして、そのタスクが実行されます。
-
ループ:上記のプロセスが終了すると、イベントループはタスクキューに他のタスクがあるかどうかをチェックします。キューに他のタスクがある場合、そのタスクが実行されます。これを繰り返してループします。
このようにして、イベントループはJavaScriptのメインスレッドが常に忙しく、非同期操作を適切にスケジュールする役割を果たしています。
なお、JavaScriptはマクロタスクとマイクロタスクの2つのタスクキューを持っています。
マクロタスクとマイクロタスク
イベントループの中で大きな括りとされるタスクキューは2つの種類に分けられます。
マクロタスク
マクロタスクは一般的に大きなタスクや全体的なタスクを指します。これには以下のようなものが含まれます:
- 全体のスクリプト(最初に読み込まれるスクリプト全体)
- setTimeoutやsetIntervalによるコールバック
- requestAnimationFrameによるコールバック
- ユーザインタラクション(クリックやキーボードイベントなど)
マイクロタスク
一方、マイクロタスクはより小さなタスク、または他のタスクの実行後に実行する必要があるタスクを指します。これには以下のようなものが含まれます:
- Promiseによるthenやcatchのコールバック
- MutationObserverによるコールバック
- queueMicrotask関数によるコールバック
実行の順番
さて、2つのキューを紹介したのでこの2つのキューの所持順番について説明します。
順番を理解する為に以下の処理をみてください。
console.log('1: 開始');
setTimeout(() => {
console.log('2: setTimeoutのコールバック');
}, 0);
Promise.resolve().then(() => {
console.log('3: プロミスのコールバック');
});
console.log('4: 終了');
さて、どのような順番でconsole.logが出力されるでしょうか。結果は以下になります。
1: 開始
4: 終了
3: プロミスのコールバック
2: setTimeoutのコールバック
1行目と4行目は予想通りですが、3行目と4行目の順番が気になりますね。
setTimeoutの秒数指定が0ですし、Promiseも定義直後にresolve()関数を実行しているので、どちらも待ち時間無しで実行されるように見えますが、何故順番が前後しているのでしょうか。
この順番がまさにマイクロタスクとマクロタスクの処理順番に繋がります。
以下、処理順番の図解になります。
上記の図解で解るように、マクロタスク内の処理が完了した後に、次のマイクロタスクが実行されます。そしてマイクロタスク内の処理が完了した後に、次に控えているマクロタスクを実行する、といった流れになります。
先ほどのコードで言うと、以下のような流れで処理が行われています。
1. マクロタスクの実行
まず最初のマクロタスクとして、全体のスクリプトが実行されます。
シングルスレッドなので上から順に実行されていき、setTimeoutとPromiseはバックグラウンドに回されて処理の完了を待つので今回のキューでは処理は実行されません。
setTimeoutもPromiseも即座にコールバック関数が実行されるので、次回以降のキューに控えて実行を待ちます。
2. マイクロタスクの実行
最初のキューであるマクロタスクが実行完了後、次のキューに移ります。次のキューはマイクロタスクなので、マイクロタスクに分類されるPromiseのコールバック関数が実行されます。
3. マクロタスクの実行
2番のキューであるマイクロタスクの実行完了後、次のキューに移ります。次のキューはマクロタスクなので、マクロタスクに分類されるsetTimeoutのコールバック関数が実行されます。
以上がマクロタスクとマイクロタスクの処理順番の解説になります。
最後に
以上でイベントループについてJavaScriptの特性について順を追いながら解説した内容になります。
イベントループを理解して、処理の順番を意識した実装をすることで予期せぬ挙動を防ぐことが出来ます。