JavaScriptの非同期処理を完全理解する

クライアントJavaScriptの非同期処理の裏側の完全理解のために、まずは実行環境がどのようになっているかを見ていきます。

クライアントJavaScriptの実行環境は、ざっくり分けると下記4つのものから成り立っているようです。

  • JavaScript Engine
  • Web APIs
  • Task QueueとJob Queue
  • Event Loop

これらの関係性を鳥瞰図で雑にまとめてみました。 f:id:shun-prog:20210503174525p:plain

これだけだと意味がわからないので1つ1つ見ていきましょう!

JavaScript Engine

JavaScript Engineとは、JavaScriptのコードを実行するプログラムのことです。
2021年5月現在、Google Chromeで使用されているものは、V8 と呼ばれるもので、C++で書かれています。
github.com

JavaScript Engineの役割は、メモリの割り当てとCall Stackの処理を実行することです。

メモリの割り当てを完全理解するのはちょっとしんどいのでCall Stackについて見てきます。

Call Stack

Call Stack(コールスタック)とは、JavaScriptのコードの実行履歴のようなものになります。

実行履歴と言っても全ての履歴がずっと残っている訳ではなく、関数から実行結果が返されると、Call Stackからはpop(最後に積まれたものから取り除かれる)されます。

Call Stackを可視化しながら JavaScriptのコードを実行できるLoupeというサイトで見てみるとわかりやすいです。

Call StackはChromeの開発者ツールからも確認できます。

f:id:shun-prog:20210502115921p:plain
開発者ツールのSourcesタブ

Web APIs

Web APIJavaScript Engineではなく、ブラウザが提供している機能であり、setTimeoutや HttpRequestなどの実装が含まれています。

Web APIで提供されている処理の場合、Web API側にコールバック関数を渡してCall Stack内の処理は終了します。

コールバック関数はWeb API上にスケジュールされ、Web APIからTask Queueと呼ばれる待ち行列にenqueueされます。

この時、setTimeoutで時間を指定している場合は、timeとして指定された秒数をWeb API上で待機してからTask Queueにenqueueされます。

そのため、setTimeoutの時間指定は、Task Queueにenqueueするまでの待機時間を指定しているということになります。 f:id:shun-prog:20210503181118p:plain

Task QueueとJob Queue

Task Queueは、WebAPIからenqueueされたJavascriptコードの待ち行列です。

キュー内に溜まったタスクは、Event Loopと呼ばれるものによって、FIFO形式で処理されます。

キューにはもう一つ似たようなものがあり、それは、Job Queueと呼ばれます。

Task QueueがsetTimeoutやclickイベントなどのコールバック関数を保持するのに対して、Job QueueはPromiseやqueueMicrotaskなどのコールバック関数を保持します。

Promiseの場合も、setTimeoutと同様にCall Stack内の処理は即時に実行され、コールバック関数がWEB APIにスケジュールされます。

WEBAPI上でresolveされると、setTimeoutで利用するQueueとは別のJob Queueにenqueueされます。

Task QueueはMacrotasks(マクロタスク)、Job QueueはMicrotasks(マイクロタスク)とも呼ばれます。

Event Loop

Event Loopは、常にCall StackとQueueを監視します。

役割としては、Call Stackが空になった時に、TaskQueueの先頭のタスクをdequeueし、Call Stackにpushします。

ここで注意が必要なのは、Call Stackが空でない場合は、TaskQueueからdequeueしないということです。

そのため、基本的に非同期の処理は同期処理が全て実行された後(= Call Stackが空)の場合に実行されます。

もう一つの注意点は、Job Queueにタスクが存在する場合は、Job Queueのタスクを優先して処理するということです。

setTimeoutとPromiseのどちらが先に優先されるかどうかを実行してみるとわかりやすいです。

よって、Job Queueにタスクが存在する限りはTask Queueは実行されません。

Promiseチェーンで複数の Job Queueタスクを実行するようなコードの場合は、全てのJob Queueが実行されるまでTask Queueは待機します。

終わりに

ここだけ抑えておけば、非同期処理での処理の順番を間違えたりすることはないかなと感じました。

JavaScriptはとても奥が深い...

間違えてる部分がありましたら是非コメントをください!

参考URL

developer.mozilla.org dmitripavlutin.com meetup-jp.toast.com www.youtube.com