JavaScriptでのイベントループ


JavaScriptの仕組み


C、Java、Pythonなどの同期言語は、個別のスレッドやプロセスを使用しない限り、予め作成された順序(同期)でコードを実行します.すなわち,後で記述するコードは,先に記述したコードより先に実行されない.
しかしJavaScriptは非同期言語です.JavaScriptは基本的に単一スレッドで実行されます.1つのプライマリ・スレッドのみで構成され、一度に1つのタスクしか実行できません.他のタスクに手を出すことはできません.次のタスクを実行するには、既存のタスクを完了する必要があります.これらのJavaScriptの非同期性によっては,先にコードを実行する操作が完了する前に,後でコードを実行する操作が先に完了する可能性がある.
function first() {
  setTimeout(() => {
    console.log("The First function has been called.");
  }, 1000);
}

function second() {
  setTimeout(() => {
    console.log("The Second function has been called.");
  }, 500);
}

first();
second();
上からfirst()、後からsecond()と呼びます.
ただし、上記のコードの実行結果は以下の通りです.
The Second function has been called.
The First function has been called.
実際のfirst()second()の実行順序が呼び出し順序と異なるかどうかを知るには、JavaScriptの呼び出しスタックとイベントループを知る必要があります.

コールスタック


JavaScriptは、呼び出した関数をスタック形式の呼び出しスタックに追加し、実行する関数(Pop)を削除します.
function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7));
bar()が呼び出されると、barのパラメータおよび領域変数を含むフレームがcallスタックにプッシュされ、barfooを呼び出し、fooのパラメータおよび領域変数を含むフレームがcallスタックにプッシュされる.fooの実行が終了すると、fooはcallスタックからポップアップされ、barフレームもcallスタックからポップアップされ、callスタックは空になります.
bar呼び出し→foo呼び出し→foo終了→bar終了
callstackとは何か知っている以上、上のコードをもう一度見てみましょう.
function first() {
  setTimeout(() => {
    console.log("The First function has been called.");
  }, 1000);
}

function second() {
  setTimeout(() => {
    console.log("The Second function has been called.");
  }, 500);
}

first();
second();
JavaScriptはcallスタックが1つしかないと言っていますが、first()second()setTimeout(() ⇒ {}, time)を同時に管理するにはどうすればいいのでしょうか.

実際、上図のように、Memory HeapやCall Stackを含むJavaScriptエンジンに加えて、JavaScriptの実行に関連する要因もいくつか存在します.Wep API、Event Loop、Callback Queue.setTimeoutを実行すると、JavaScriptエンジンはsettimeoutのコールバック関数と時間をWeb APIに送信します.その後、Web APIはsettimeoutタスクを実行し、設定された時間(1000ms or 500ms)の後にコールバック関数(console.log(...))をCallback Queueに渡す.

コールバックキューとイベントループ


event loopは、コールバックスタックが空になるまで、コールバックキュー内のコールバック関数(メッセージ)を処理し続けます.
イベントループの実装方法は次のとおりです.
while (queue.waitForMessage()) {
  queue.processNextMessage();
}
イベントループは、キューに新しいメッセージがあるたびに次のメッセージを処理します.
キューにメッセージがない場合は、同期して新しいメッセージの到着を待機します.

Run to completion scheduling(nonpreemptive scheduling)


JavaScriptのコールバックキューでは、現在のメッセージが完全に処理されるまで、次のメッセージの処理が開始されません.
メッセージ処理時間が長すぎると、Webブラウザに次のエラー画面が表示されます.

このような状況を回避するためには、メッセージ処理時間をできるだけ短縮し、1つのメッセージを複数のメッセージに分割する必要があります.
JavaScriptはsettimeoutの2番目の引数の時間値の遅延を保証しません.時間の最小遅延を表すだけです.
理由はJavaScriptのRun to Completionスケジューリングです.
イベントloopはRun to completionに従ってスケジューリングされるため、キュー内のタスクは順次処理され、ダイヤルバックメッセージが処理されます.

参考資料


JavaScript動作原理(Single Thread、Event Loop、Asynchronous)
非同期Javascript-単一スレッドベースのJSの非同期処理方法-Hudi-類似プログラマー
モデルとイベントループの同期