Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8916

JavaScriptのイベントループを理解する

$
0
0

イベントループとは?

JavaScriptはシングルスレッドです。一度に実行できるタスクは1つだけです。つまり、

  • 2つ以上の処理を並行して実行できない
  • 2つ以上の関数を同時実行できない

ということになります。

例えば、10秒掛かるタスクを実行すると、他のタスクは10秒間待機しなければなりません。
JavaScriptはデフォルトでブラウザのメインスレッドで実行されるため、UI全体が動かなくなります。

では、イベントループについて解説していきます。
まず、V8などのJavaScriptエンジンやブラウザには、下記の画像の様にコールスタック(Call Stack)ヒープ(Heap)タスクキュー(Task Queue)Web APIと呼ばれる4つのメカニズムを備えています。

JavaScriptエンジン

まずは、このメカニズムを簡単に説明していきます。

Web API

Web APIにはDOM Event, setTimeout, Ajaxなどが含まれます。これらの機能は非同期(ノンブロッキング)に実行されます。
ブラウザが提供する機能です。

ヒープ(Heap)

動的に確保と解放を繰り返せるメモリ領域です。
オブジェクトはヒープに割り当てられています。
JavaScriptエンジンの内部に実装されています。

コールスタック(Call Stack)

関数は呼び出されるとコールスタックに追加されます。
コールスタックはLIFO(後入れ先出し)で機能します。
JavaScriptエンジンの内部に実装されています。

タスクキュー(Task Queue)

コールバック関数はタスクキューで待機します。
タスクキューはFIFO(先入れ先出し)の配列で、イベントループは、コールスタックが空になる度に、タスクキューからコールバック関数を取り出して実行します。
JavaScriptエンジンの外部に実装されています。

MDNでは、なぜ「ループ」という名前が付いたか、下記の様な仮想コードで説明しています。

while(queue.waitForMessage()){queue.processNextMessage();}

上記のwaitForMessage()は、現在実行中のタスクが存在しない場合、次のタスクがタスクキューに追加されるまで待機します。このようにイベントループは、「現在実行中のタスクがないこと」と「タスクキューにタスクがあるか」を繰り返し確認します。簡単にまとめると次のようになります。

  • すべての非同期APIは作業完了後、コールバック関数をタスクキューに追加する。
  • イベントループは、現在実行中のタスクがない(コールスタックが空になった)場合、FIFOでタスクキューから取り出して実行する。
  • 現在実行中のコールタスクがある(コールスタックに関数がある)場合、LIFOでコールスタックから取り出して実行する。

これらがイベントループの簡単な説明になります。
では、このメカニズムを実際のコードを踏まえて理解していきましょう。

イベントループをコードで理解する

プログラム1

まずは簡単に下記のコードの実行結果を考えてみましょう。

constfoo=()=>console.log("First");constbar=()=>setTimeout(()=>console.log("Second"),1000);constbaz=()=>console.log("Third");foo();bar();baz();

実際に実行してみましょう。実行結果は下記の様になったと思います。

"First""Third""Second"

このプログラムの流れとしては

  1. fooがコールスタックに追加されます。そして、"First"を返し、コールスタックからポップされます。
  2. barがコールスタックに追加されます。
  3. コールバック関数の() => console.log("Second")がWeb APIに追加され、1000ms待機します。
  4. その瞬間setTimeoutはコールスタックからポップされ、値を返します。
  5. 1000ms待機している間に、bazがコールスタックに追加さます。そして、"Third"を返し、コールスタックからポップされます。
  6. 1000ms後、() => console.log("Second")は直ぐにコールスタックに追加されるのではなく、タスクキューに渡されます。
  7. そして、イベントループで、コールスタックが空なことを確認し、タスクキューにある() => console.log("Second")がコールスタックに渡されます。
  8. () => console.log("Second")"Second"を返し、コールスタックからポップされ、プログラムは終了します。

となります。視覚的に理解したい方は、こちらで上記のプログラムを実行してみると面白いと思います。
では、次のプログラムにいきましょう。少し難易度が上がります。

プログラム2

下記のプログラムを実行し、ボタンをクリックしてみましょう。

<!DOCTYPE html><head><title>event loop</title></head><body><buttonid="heavyCount">click</button><divid="counter">0</div><script>constbutton=document.getElementById('heavyCount');constcounter=document.getElementById('counter');button.addEventListener('click',function(){letcount=0;lettimes=1000;functionloop(){if(count++<times){counter.innerHTML=count;loop();}else{alert("Done");}}loop();});</script></body></html>

恐らく、アラートが表示された後、下記の様にページ内の0が1000に変化したと思います。

a6cb49d5b20cc69f661d337d8b57cf47.gif

では、このプログラムを下記の様に動かしたいときはどうでしょう。ソースコードを少し修正してあげるだけで、この動きは実現できます。

26ad84952b606021e1665555cb5dcfff.gif

その前に、イベントループにおける、最初の動きの流れを追っていきたいと思います。

  1. まず、addEventListenerをコールスタックに追加。
  2. addEventListenerのコールバック関数をWebAPIに追加。
  3. ボタンをクリックすると、loopがコールスタックに追加されます。
  4. countが++されます。
  5. counter.innerHTMLがタスクキューに追加されます。
  6. 次に、loopの中で更にloopを再帰的に呼び出しています。
  7. loopがコールスタックに追加されます。
  8. countが++されます。
  9. counter.innerHTMLがタスクキューに追加されます。
  10. 次に、loopの中で更にloopを呼び出します。
    …という風に、今回のプログラムでは、コールスタックにcountが1000になるまで、つまり、1000個のloopがコールスタックに追加されます。
  11. そして、countが1000になったところで、else内のalert("Done")が実行されます。
  12. コールスタックが空になったところでタスクキューで待機しているcounter.innerHTML達を実行していきます。
  13. 最後に1000が表示されてプログラムは終了します。

プログラム2-2の解答

では、プログラム2-2の解答です。

// ...省略functionloop(){if(count++<times){counter.innerHTML=count;setTimeout(loop,0);// setTimeoutを追加}else{alert("Done");}}

setTimeoutのコールバックとしてloopを渡していますね。
では、何故これだけで上記の動きが実現できるのでしょうか。

今までは、コールスタックにloopがどんどん追加されていき、その間にinnerHTMLがタスクキューにどんどん追加されていきました。
これは、タスクにあるloopを全て処理仕切ったとき、つまり、countが1000になった後、タスクキューで待機していたinnerHTMLでレンダリング処理をするという流れになります。ここはさっき説明したので、問題ないと思います。

しかし、再帰呼び出しでsetTimeoutを挟むことによってloopはコールスタックではなく、WEB APIを経由した後、タスクキューに追加されていきます。
つまり、タスクキューには、innetHTMLloopinnetHTMLloop→ ... という風に、追加されていきます。では、この部分の処理の流れを追っていきましょう。

  1. まず、innerHTMLが実行されます。countは1なので、1が画面にレンダリングされます。
  2. loopをコールスタックに追加します。countを++します。
  3. コールスタックが空になりましたのでタスクキューで待機しているinnerHTMLを実行します。countは2なので、2が画面にレンダリングされます。
  4. これをcountが1000になるまで実行します。

という風に、setTimeoutを使用することで、loopの間にinnerHTMLによるレンダリング処理を追加することができます。これで、2枚目の動きが実現できる訳ですね。

スタックオーバーフロー

このコールスタックは、無限に関数を積み重ねることはできません。何事にも限界はあります。コールスタックの許容量を超えてしまうと、スタックオーバーフロー(StackOverflow)というエラーが発生します。これは関数の再帰的な実行を行うときによく出てきます。

今回話せなかったこと

  • マイクロタスク(micro task)
  • ウェブワーカー(Web Worker)
  • setTimeoutの誤差

この3つもイベントループに関係してきますので、気になる方は勉強してみると面白いですよ。
僕も機会があればまた記事を書こうと思います。

参考文献


Viewing all articles
Browse latest Browse all 8916

Trending Articles