この記事ではJavaScriptのイベントループベースの並行モデルの基礎を紹介した上、Promiseメソッドが一般的な非同期処理の実行順序の違いについてお話す
※ この記事にある英語を含める画像はほとんど以下のコースから取得した
The Complete JavaScript Course 2021: From Zero to Expert! @ Udemy
Promiseメソッドは何か
MDNにより、
Promise オブジェクトは非同期処理の最終的な完了処理 (もしくは失敗) およびその結果の値を表現します。
promiseを宝くじ券と考えると、当たる(fulfilled)場合と当たらない(rejected)場合がある
- 当たる(fulfilled)場合、thenメソッドが呼び出される
- 当たらない(rejected)場合、catchメソッドが呼び出される
- 当たる・当たらないと関係しなくfinallyメソッドが呼び出される
(then method only called when promise is fulfilled, catch method is only called when the promise is reject, finally method always been called no matter the result of the promise.)
Promiseメソッドの処理順序
まずは一般的な非同期処理はどの仕組みで実行されるか見ていきましょう
JavaScript(JS) の実行について
Javaがランタイムを持つと同様に、JavaScriptはJSランタイムを持っている。このJSランタイムは実ブラウザの上にある。
Node.jsの例で言うと、
Node.js is a platform built on Chrome's JavaScript runtime for easily building fast and scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.
また、MDNにより、
従来より JavaScript はシングルスレッドです。複数のコアを利用しても、メインスレッドと呼ばれる単一のスレッド上でタスクを実行できるだけでしょう。これまでの例は、次のように実行されます。
ということで、JSでは同時並行処理ではなく、イベントループに基づく同時実行モデル仕組みであり、JSランタイムの中でタスクの実行順序を調整される。
具体的にどういう仕組みかを分かるため、JSランタイムを詳しく見ていきましょう。
JSランタイム
JSランタイム中に主にJSエンジン、Web APIs、イベントループ、キュー四つのコンポーネントが存在する。
JS全体なイメージは以下の通り
それぞれの役割は、
JSエンジン
- JSランタイムのハットになり、エンジンの中にさらにヒップとコールスタックが存在する
- ヒップはオブジェクトの保存場所、動的にメモリ領域を確保・解放する
- コールスタックは、
- インタープリターの仕組みの一つ
- 複数階層の関数を呼び出したスクリプト内の位置を追跡し続けること
- 関数が呼びされるとここに追加、値が返された時にpopする
- LIFOの原則でに基づく機能する
- グローバルエクスキューションコンテキストを持つ
Web APIs
Webブラウザに組込まれているAPI、非同期処理の実行場所。ここでメインにクライアントサイド(ブラウザ)のJSでのAPIを指す。
(これ以外に、サードパーティAPI(Twitter APIなど)という分類もある)
よく使われるWeb APIが、例えば、
- ブラウザで読み込んだ文書を操作するためのDOM
- サーバからデータ取得をするAPIのXMLHttpRequestやFetch API
キュー
実行待ちのコールバック関数を持つキュー。主にはコールバックキューとマイクロタスクキュー2種類がある(以下の図をご参照)。
- 一般的なFIFOの原則でに基づく機能する
- WEB APIでは実行される関数は、実行時点で引数にコールバック関数を取ることが多い。これらのコールバック関数はすぐにコールスタックに追加されるのではなく、まず、Web API環境で非同期処理が実行される、そして関連のコールバック関数をキューに渡される
イベントループ
コールスタックとキューの間のタスクコーディネーター機能する。
- コールスタックが(グローバルエクスキューションコンテキスト以外に)空白かどうかを確認
- 空白の場合、あるいは実行中のコードがない場合コールバックキューからタスクを取得し、コールバックに置く
この流れは1つのevent loop tickと呼ばれる
Promiseメソッドの実行と一般的なタスクとの違い
一般的になタスクはコールバックキューに、Promiseメソッド(Promise以外もある)のコールバック関数はマイクロタスクキューに渡される。
マイクロタスクキュー内のタスクはコールバックキューのタスクより優先度が高い。
つまり、一event loop tickの後、イベントループがキューからタスクをコールスタックに調達する時、WEB APIがマイクロタスクキューにタスクが継続に追加されるさえ、コールバックキューのタスクが永遠に調達・実行されないこと
コールバックキュー
マイクロタスクキュー
JSコード実行の流れを例で説明
el = document.querySelector('img');
el.src = 'dog.jpg';
el.addEventListener('load', () => {
el.classList.add('fadeIn');
});
fetch('https://url.com/api')
.then(res => console.log(res));
こちらのコード実行の流れは以下の通り、
- イメージのロードは、WEB API環境でバックグランドで走らせ、非同期(このシーンでnon-blockingも言う)形式でローディングされる
- ロードイベントのコールバック関数はまずWEB APIに登録され、ロードイベント発火までWEB API環境に残る
- ロードができた次第コールバック関数が実行されるため、ロード完了を待つ間その次のコード、ここではfetch関数のdata fetch、が実行される。同じくfetchのコールバック関数もWEB APIに登録される
- イメージのロードが完了したらロードイベントが発火し、この時点でロードイベントのコールバック関数をタスクとしてコールバックキューに渡される
- 同じくデータfetchingができたら、fetchのコールバック関数をタスクとしてマイクロタスクキューに渡される
- コールバックキュー、またはマイクロタスクキューに渡したタスクはすぐに実行されることではなく、行列で並んで、イベントループがコールスタックに実行中のコードがないと確認したら、先入れ先出し順序でタスクを順番にコールスタックに渡して実行される
- イベントループがキューにタスクを見に行く時点、もしマイクロタスクキューにfetchのコールバック関数が存在、コールバックキューにロードイベントのコールバック関数が存在する場合、マイクロタスクキューのfetchのコールバック関数を優先にコールスタックに渡し実行される
※ ここでfetch()の結果はPromiseで返される
参考
- Fetch の使用
- Promise
- Understand promises before you start using async/await
- How to make HTTP requests using Fetch API and Promises
- A simple guide to JavaScript concurrency in Node.js and a few traps that come with it
- JavaScriptの非同期処理を並列処理と勘違いしていませんか?
- How JavaScript works: an overview of the engine, the runtime, and the call stack
- How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code
- JavaScript の仕組み:メモリ管理+ 4つの共通のメモリリーク処理方法
- Even with async/await, raw promises are still key to writing optimal concurrent javascript
- The Javascript Runtime Environment