JavaScriptの非同期処理はAsync Awaitから学習するな!
はじめに
私が非同期処理を理解するのに躓いたポイントなどを踏まえながら、非同期処理について学習したことをまとめています。
ここではAsync Await Promiseについては記述してません。(別の記事で書きたい!)
駆け出しなので、間違いなどあれば、どんどんご指摘ください!!!
なぜAsync Awaitから学習したら良くないの?
理由は2つです。
- Async Awaitは非同期処理を実現するための記法にすぎない
- 簡略化されていて何が起こっているか分かりにくい
外部のAPIと通信するときなど、なんとなくでAsync Awaitを使っていたのですが、JavaScriptの非同期処理がどのような仕組みで動いているのかが理解できていなかったです。
非同期処理を理解する上で大事なワード
非同期処理で大事になるワードです。
- スレッド
- 実行コンテキスト
- コールスタック
- タスクキュー
- イベントループ
- WebAPI
スレッド
プログラムの開始から終了までの一連の処理の流れのこと
処理の流れを1本の糸のように表していることからスレッドという名前になっています。
JavaScriptのコードが実行されるスレッドのことをメインスレッドと呼びます。
基本的にJavaScriptはシングルスレッドで実行されます。
シングルスレッドとは1つのスレッドで処理を行うことです。
シングルスレッドだとどんな問題が起きる?
シングルスレッドでは前の処理が完了しないと次の処理に移ることができません。
例
図のように、処理Bがデータを取得する時間のかかる処理の場合、処理Cは処理Bの完了を待つ必要があります。処理Cでは画面の表示を実行しているので、処理Bが完了するまで、画面にはなにも表示されなくなってしまいます。
つまり、シングルスレッドでは、時間のかかる処理が間にはいると、その処理の完了まで、次の処理には進めないため、効率が悪くなってしまいます。
例えるなら、レジ待ちの長蛇の列ですかねw
自分は1個しか買っていないのに、前の人がたくさん買っていると、レジに時間がかかるので、もうひとつレジがほしいーとなりますよね。
処理をどうにかして切り離したい、この問題を解決するのが非同期処理になります。
非同期処理
JavaScriptでの非同期処理とは、メインスレッドから一時的に切り離された処理のことを指します。
例 (setTimeout関数)
setTimeoutは非同期処理を実行できるWebAPIです。
setTimeout関数が実行されると、一時的にメインスレッドから処理が切り離されます。
切り離された後、メインスレッドでは処理Cが実行されます。
その後、指定されたミリ秒後にメインスレッドに処理が戻り、コールバック関数が実行されるという流れです。
この一時的にメインスレッドから処理を切り離すことが非同期処理ということになります。
では、切り離された処理はどこに格納されているのでしょうか?
切り離された処理は誰が実行しているでしょうか?
それを知るには、イベントループを理解する必要があります。
イベントループ
イベントループについて解説する前にイベントループを理解するうえでかかせない概念を解説します。
実行コンテキスト
JavaScriptエンジンによって準備されるコードの実行環境のことです。
実行コンテキストにはどのような状態でコードが実行されているのかという情報が保持されます。
ブラウザの実行環境ではWindowオブジェクトなどが使えるのもこの実行コンテキストが生成されるからになります。(グローバルコンテキストと関数コンテキストでは保持する情報が変わります)
実行コンテキストは基本的には2種類に分かれます。
- グローバルコンテキスト
script直下やトップレベルのJavaScriptコードが実行される前に生成される - 関数コンテキスト
関数が実行される前に生成される
コールスタック
コールスタック (call stack) は、インタープリター (ウェブブラウザー内の JavaScript インタープリターなど) の仕組みの一つで、複数階層の関数を呼び出したスクリプト内の位置を追跡し続けることです。 — どの関数が現在実行されているのか、その関数の中でどの関数が呼び出されたか、などです。
MDNでは上記のように書いてあります。
つまり、JavaScriptエンジン上で実行コンテキストが積み重なっている状態のことをコールスタックだと呼びます。
実行コンテキストが生成されると必ずコールスタックに追加されます。
タスクキュー
非同期処理の関数が格納されるキューのことです。
非同期処理がメインスレッドから切り離された後、どこに格納されているのという疑問があったと思いますが、このタスクキューに格納されているのです。
キューは、最初に入ったデータが最初にでていくFIFO(First In First Out)というデータ構造になります。
対象的にLIFO(Last In First Out)という構造もあり、最初に入ったデータが最後にでるデータ構造です。
イベントループ
コールスタックが空になったら、タスクキューに格納されたタスクキューを実行する仕組みです。
イベントループがコールスタックを監視し、タスクキューにある非同期処理を実行しています。
WebAPI
ブラウザが提供するAPIのことを指します。
WebAPIはJavaScriptエンジンが提供しているのではなく、ブラウザが提供している点がとても重要です。DOM操作、非同期通信(Ajax)、タイマー機能(setTimeoutやsetInterval)などの機能はすべてWebAPIの機能であり、JavaScriptはブラウザから提供されたものを使わせてもらっているだけということになります。
では、図を使ってそれぞれがどのように動いているのか見てみましょう!
setTimeout関数が実行されると関数コンテキストが作成され、コールスタックに追加されます。
setTimeout関数はWebAPIの非同期APIなので、コールスタックからWebAPI側に移動します。
setTimeout関数のコールバック関数がタスクキューに登録されます。
イベントループがコールスタックが空になったのを確認したら、タスクキューに登録されていたコールバック関数がコールスタックに追加され実行されます。
※ 図にはマイクロタスクキューが書かれているがここでは使っていないです。
マイクロタスクキューとはPromiseで書かれた非同期処理などが格納されるキューになります。
マイクロタスクキューはタスクキューより優先して実行されます。
Promiseを記事にするときに詳しく解説します!
コード上で非同期処理を解説
const log1 = () => {
console.log(1);
};
const log2 = () => {
setTimeout(() => {
console.log(2);
}, 0);
};
const log3 = () => {
console.log(3);
};
log1();
log2();
log3();
上記はどのような結果になるでしょうか?
setTimeoutがあるので、非同期処理になりそうです。しかし、引数に0が入っているので、待つ時間はなさそうなので、表示は1、2、3となるでしょうか?
1
3
2
実は、結果は1、3、2となります。
では、流れを見ていきましょう!
流れ
1 グローバルコンテキストが生成され、コールスタックに追加、実行される
2 log1関数が実行され、実行コンテキストがコールスタックに追加
3 log1関数内のconsole.log(1)がコールスタックに追加、実行され、log1はコールスタックから消滅する
4 log2関数が実行され、実行コンテキストがコールスタックに追加
5 setTimeout関数が関数内に書かれているので、コールスタックに追加後、WebAPIに渡される
第2引数が0なので、待つ時間もなくコールバック関数がタスクキューに登録される
6 5が走っている間にlog3関数が実行され、実行コンテキストがコールスタックに追加
7 log3のconsole.log(3)がコールスタックに追加、実行される、その後log3関数も消滅する
8 コールスタックが空になったので、イベントループが検知し、タスクキューに登録されていたコールバック関数(console.log(2))をコールスタックに追加し、実行する
そのため、setTimeoutで0秒を指定しても、1、3、2という結果になります。
ここで重要なことは、タスクキューに登録された処理はコールスタックが空になるまで実行されないということです!
最後に
次回はPromiseとAsync Awaitも記事書きたいと思ってます。
読んでいただきありがとうございました!
参考
Discussion