描いて理解するイベントループ

# はじめに

早速ですが Node.js で次のコードを実行したときの出力がどうなるのかわかるでしょうか？

```js:setTimeoutに0秒を指定
console.log("start");

setTimeout(function() {
  console.log("Hello!!!");
}, 0); // 第２引数は 0 ミリ秒 = 0 秒待つ？

console.log("end");
```

実は、出力は以下の順になります。

```:実行結果
start
end
Hello!!!
```

「なんで？」と思った人向けに、順を追って解説します。
「あたりまえじゃないか」と思った人はブラウザバックしてしまいましょう。[^help]

[^help]: ずっと Java をやっていて JavaScript はほぼ初心者です。初心者なりの理解を書いた記事なので、誤りがあったらコメント欄でやさしく教えていただけると嬉しいです。

# 1. イベントループ

この記事では JavaScript の実行環境を「ワンオペで黙々と作業をこなす従業員」に例えて説明します。

まずは次の図を見てください。

<img src="https://camo.qiitausercontent.com/13661dccddba32ddad1a3fd48941bb68e5f78250/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f61386139636564392d376632302d633939362d613631612d3261333661373138373565382e706e67" alt="EventLoop-001_outline.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/a8a9ced9-7f20-c996-a61a-2a36a71875e8.png" loading="lazy" width="600">

オフィスの片隅でベルトコンベアに乗って運ばれてくる資料を黙々と処理しつづけています。
作業内容は資料に書かれているので席を立つ必要もありません。

<img src="https://camo.qiitausercontent.com/90721d1d58cfe578f9e6c6d6fbed7b27a05f7a52/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f64303634663566362d653466342d346535392d303735352d6532646539626263383065392e706e67" alt="EventLoop-002_poll.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/d064f5f6-e4f4-4e59-0755-e2de9bbc80e9.png" loading="lazy" width="600">

もちろん休憩時間は与えられないので、ひとつのタスクが片付いたらすぐに次の作業に取り掛かります。

### ポイント

ここではとりあえず、以下の点だけ認識しておいてください。

- タスクは一方向に流れてくる
- 一人きりで働いている[^singlethread]
- 机が空くまで[^callstack]次のタスクに着手できない

[^singlethread]: シングルスレッドなので
[^callstack]: コールスタックが空になるまで。作業中はメモや資料などが積み重なってる（スタックしている）イメージなので机で例えています。


### イベントループとは何か？

では、イベントループとはなんなのか？図に書いたとおり、JavaScript の処理というのは以下のステップを繰り返しています。この繰り返しのことを `イベントループ` と呼ぶのです。メインスレッドではひたすらループを回し、キューに溜まっているタスクを取り出しては実行していきます。

1. タスクを処理する
2. タスクが完了したら次のタスクを取り出す

# 2. イベントハンドリング

ブラウザでのイベントハンドリングを考えます。

ボタン `<button id="btn">` をクリックしたときにログ出力する、というのは次のようにして実現できます。

```js:イベントハンドラの登録
document.getElementById("btn").addEventListener("click", function ()   
  console.log("ボタンがクリックされました");
});
```

このコードが実行された時点では `click` イベントにコールバック関数が紐付けられただけです。ボタンをクリックすると下図のようにコンベア（キュー）の最後尾にその関数が追加されます。重要なのは、クリックしたときに関数が実行されるのではなく **クリックしたときに関数をキューに追加する**[^function] ということなのです。

[^function]: キューで管理されるのは関数そのものなのか自信ありませんが、定性的な理解としては大きく間違っていないと思っています。

<img src="https://camo.qiitausercontent.com/de8a18cf5190c9e9a4fcfca1b2d6a60e6d47fda8/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f33313031323730372d393736302d393362302d616331612d3864303433643964666364322e706e67" alt="EventLoop-003_push_callback.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/31012707-9760-93b0-ac1a-8d043d9dfcd2.png" loading="lazy" width="600">


### イベントハンドラはブロックされる

`click` イベントが発火した時点ではコールバック関数（イベントハンドラ）はキューに追加されるだけなので、それより前にあるタスクが片付かないと実行されません。

<img src="https://camo.qiitausercontent.com/abd5b9a55bc34eb33354f0ba0a0ddc502a0f75a9/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f65313337333965342d653338622d306535312d613664652d6263323164656133616235362e706e67" alt="EventLoop-004_wait.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/e13739e4-e38b-0e51-a6de-bc21dea3ab56.png" loading="lazy" width="600">

このことを確認するためのサンプルコードを載せておきます。

デベロッパーツールでコンソールを見ながら操作してみるとわかりますが、`イベントハンドラを登録しました` と表示されてから 5 秒経つまでは、何度ボタンをクリックしても時刻は表示されません。そして、５秒経過したらボタンをクリックした回数分だけハンドラが実行され、ログ出力が行われます。

```html:イベントハンドラがブロックされる例
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <script>
    window.onload = function () {
      document.getElementById("btn").addEventListener("click", function () {
        console.log(new Date().toISOString()); // 現在時刻を出力
      });
      console.log(`${new Date().toISOString()} : イベントハンドラを登録しました`);

      // 5 秒間ループ
      var start = Date.now();
      while (Date.now() - start < 5000) {
        // ループ中はボタンをクリックしてもログ出力されない
      }

      console.log(`${new Date().toISOString()} : タスク完了`);
    };
  </script>
</head>

<body>
  <button id="btn">クリックすると現在時刻がログ出力されます</button>
</body>

</html>
```

このことからわかるように、JavaScript ではひとつのタスクに時間をかけると他のすべてのタスクに影響が出てしまいます。ブラウザで動かす場合も、Node.js などでサーバーサイドで動かす場合も、イベントループは絶対に止めてはいけません。

# 3. setTimeout

さて次は `setTimeout` です。
これもさっきの `click` イベントのハンドラと似たことなのですが、時間に関係するイベントは管理方法が少し異なります。

<img src="https://camo.qiitausercontent.com/7854761070cb28216f511aaad1b4034be36d55b4/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f38333232313730382d643035612d323932622d363764612d3732626536613161366232312e706e67" alt="EventLoop-005_time_event.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/83221708-d05a-292b-67da-72be6a1a6b21.png" loading="lazy" width="800">

`setTimeout` の第一引数で渡されたコールバック関数は、キューに追加されるのではなく実行予定の時刻をメモして箱にしまわれます。実はタスクが終了して次のループに入るとき、タスクを取り出す前に時刻がチェックされます。

<img src="https://camo.qiitausercontent.com/a3048cead63744ba1cd6b348350bc41815bf3148/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f65303562373133622d643232372d316338332d366566622d3539326164663635376561302e706e67" alt="EventLoop-006_timer_phase_1.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/e05b713b-d227-1c83-6efb-592adf657ea0.png" loading="lazy" width="800">

時刻をチェックしたときに「すでに実行時刻を過ぎているもの」が存在すれば、そのタスクが実行されます。[^timeout]

[^timeout]: 実行時間を過ぎていたらキューに追加する、の方が正しいかも。このあたりちゃんと理解できてないです

<img src="https://camo.qiitausercontent.com/6b6150b889ace957d7345392123ccbee554f1c90/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f66613537633737612d623634622d386166662d666132652d3566666136386232383665662e706e67" alt="EventLoop-006_timer_phase_2.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/fa57c77a-b64b-8aff-fa2e-5ffa68b286ef.png" loading="lazy" width="800">

### setTimeout は「○○秒後に関数を実行する」のではない

さきほどの `click` イベントの例と同じですが、イベントループが進まなければ時刻のチェックも行われません。たとえば次のコードを実行すると、１秒後に実行したい処理が５秒経たないと実行されません。処理が時間通りに行われなくなってしまうので、やはり何があってもループを止めてはならないのです。

```js:ブロックされる例
console.log("start");
setTimeout(function() {
  console.log("１秒経ちました");
}, 1000); // 1 秒後に実行したい

// 5 秒間ループ
var start = Date.now();
while (Date.now() - start < 5000) {
}
console.log("５秒経ちました");
```

<br>

# はじめの問題

ここで、はじめの問題に戻ってみましょう。

```js:setTimeoutに0秒を指定
console.log("start");

setTimeout(function() {
  console.log("Hello!!!");
}, 0); // 第２引数は 0 ミリ秒 = 0 秒待つ？

console.log("end");
```

```:実行結果
start
end
Hello!!!
```

ここまで読んだ方なら、何故この順番に出力されるのか理解できるのではないかと思います。この場合の処理をイラストにしてみたので、ひとつずつ処理を確認してみてください。

<img src="https://camo.qiitausercontent.com/eaf0d0fbc134d4c9c9e283b7d0aef18d18cc936d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3737313435362f33626331313832302d306262352d643036362d396563322d6339326639303232636434662e706e67" alt="EventLoop-007_setTimeout.png" data-canonical-src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/771456/3bc11820-0bb5-d066-9ec2-c92f9022cd4f.png" loading="lazy" width="300">

# おわりに

もともとは Node.js の勉強中に書いていた個人的なメモを書き直したものなので、他人が見て理解できる図になっているのか不安です。
それでも、自分と似た感性の人にとって理解の一助となれたら嬉しいです。

## 検証環境

この記事では、特に断りのない場合以下の環境で動作確認を行っています。

* Node 15.0.1
* Firefox 82.0

# 参考

* [What the heck is the event loop anyway?](https://www.youtube.com/watch?v=8aGhZQkoFbQ)<br>2014年の JSConf でのセッション。英語ですが日本語字幕もあり非常にわかりやすいです
* [The Node.js Event Loop, Timers, and process.nextTick()](https://nodejs.org/ja/docs/guides/event-loop-timers-and-nexttick/)<br>Node.js の公式ドキュメント
* [並行モデルとイベントループ](https://developer.mozilla.org/ja/docs/Web/JavaScript/EventLoop)<br>MDNの解説