JavaScriptの非同期処理に関しての理解
背景
フロント開発で、よくPromise、await、then、catch、finally、asyncなどのキーワードがありますね、これらと一緒に出る単語は”同期処理”、”非同期処理”ですね。
多分Javaでの逐次処理、並列処理、並行処理と類似しているものと最初勝手にそう思いました。
実際にフロントエンドの開発を始め、この非同期処理の理解についてすごく苦労してました。(ただのプロセス上コードを一行ずつに実行するか、プロセスが複数に同時に処理することではなかったですね😭)
いきなり非同期処理クイズです!
まず下記のソースコードの実行順を予測してみてください!
// timeandpromise.js
console.log("[A] 🦖 MAINLINE: Start");
setTimeout(() => {
console.log("[B] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("1st Promise")
.then((value) => console.log("[C] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[D] 👦 MICRO: Next chain"));
setTimeout(() => {
console.log("[E] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("2nd Promise")
.then((value) => console.log("[F] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[H] 👦 MICRO: Next chain"));
});
});
setTimeout(() => {
console.log("[I] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("3rd Promise")
.then((value) => console.log("[J] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[K] 👦 MICRO: Next chain"));
});
Promise.resolve().then(() => console.log("[L] 👦 MICRO: then"));
console.log("[M] 🦖 MAINLINE: End");
🙈結果🙈
~ deno run timeandpromise.js
[A] 🦖 MAINLINE: Start
[M] 🦖 MAINLINE: End
[L] 👦 MICRO: then
[B] ⏰ TIMERS: setTimeout[0ms]
[C] 👦 MICRO: Resolved value: 1st Promise
[D] 👦 MICRO: Next chain
[I] ⏰ TIMERS: setTimeout[0ms]
[J] 👦 MICRO: Resolved value: 3rd Promise
[K] 👦 MICRO: Next chain
[E] ⏰ TIMERS: setTimeout[0ms]
[F] 👦 MICRO: Resolved value: 2nd Promise
[H] 👦 MICRO: Next chain
当たった人、おめでとう🎉🎉🎉!!!今記事をクローズしても大丈夫です。もし当たったなかったら、これから一緒に謎を解けましょう!!!
クイズを解けるために、下記の知識が必要ですね
非同期処理、実際はインベントループでのタスクの処理順番を制御する仕組みです。
インベントループ
- イベントロープで複数のタスクキューがある
- イベントループで通常一つのマイクロタスクキューがある
※ こちら資料を参照している。
- https://v8.dev/blog/fast-async#tasks-vs.-microtasks
- https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
- https://www.youtube.com/watch?v=u1kqx6AenYw
タスクキュー
-
- タスクとしてのもの
- 1、イベント(Event)
- 2、パース(Parsing)
- 3、コールバック(Callbacks)
- 4、リソースの使用(Using a resource)
- 5、DOM 操作への反応(Reacting to DOM manipulation)
-
- タスクがタスクキューに追加タイミング
- 1、新しい JavaScript のプログラムやサブプログラムが直接的に実行される時(コンソールからや、スクリプト要素内のコードを実行するなどの形式で)
- 2、イベントが発火し、イベントのコールバック関数がタスクキューへと追加する時
- 3、同一の供給源から来たタスクは同じタスクキューへと送られる
-
- タスクキューで重要なルール
- 1、複数あるタスクキューはどの順番に処理されるか決められていない
- 2、同一のタスクキュー内に存在しているタスクは到着した順番に処理される
- 3、タイムアウトやインターバルの時間が経過し、登録しておいたコールバックがタスクキューへと追加される時(例:setTimeout() や setInterval()など)
マイクロタスクキュー
-
マイクロタスクは、それを呼び出し関数やプログラムが実行された後にコールスタックが空になった後にのみ実行される短い関数です。
- 例:Promiseのthen(), catch(), finally()メソッドなどの引数に渡すコールバック関数がマイクロタスク
-
マイクロタスクキューとタスクキューは異なるもの
-
マイクロタスクキューはタスクキューよりも優先的に処理される(単一タスク(Task)が実行された後にすべてのマイクロタスク(Microtask)を処理する)
-
コールスタックが空になったらマイクロタスクを処理される
上記のヒントを持って、もう一度クイズを分析しよう!
// コンソールイベントはタスクとして処理される
console.log("[A] 🦖 MAINLINE: Start");
setTimeout(() => {
// コールバックはタスクとして処理される
console.log("[B] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("1st Promise")
// コールバックはマイクロタスクとして処理される
.then((value) => console.log("[C] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[D] 👦 MICRO: Next chain"));
setTimeout(() => {
// コールバックはタスクとして処理される
console.log("[E] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("2nd Promise")
// コールバックはマイクロタスクとして処理される
.then((value) => console.log("[F] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[H] 👦 MICRO: Next chain"));
});
});
setTimeout(() => {
// コールバックはタスクとして処理される
console.log("[I] ⏰ TIMERS: setTimeout[0ms]");
Promise.resolve("3rd Promise")
// コールバックはマイクロタスクとして処理される
.then((value) => console.log("[J] 👦 MICRO: Resolved value:", value))
.then(() => console.log("[K] 👦 MICRO: Next chain"));
});
// コールバックはマイクロタスクとして処理される
Promise.resolve().then(() => console.log("[L] 👦 MICRO: then"));
// コンソールイベントはタスクとして処理される
console.log("[M] 🦖 MAINLINE: End");
/*
-----------Round1-------------
タスク「コンソールA」が処理、→ A
タイマのタスク「コンソールB及び中のすべてタスク」がタスクキューに追加される、
タイマのタスク「コンソールI及び中のすべてタスク」がタスクキューに追加される、
Promiseのマイクロタスク「コンソールL」がマイクロタスクキューに追加される、
タスク「コンスールM」が処理 → M
現時点
タスクキュー:「コンソールB及び中のすべてタスク」、「コンソールI及び中のすべてタスク」
マイクロタスクキュー:「コンソールL」
-----------Round2-------------
単一タスクが終わったら、すべてのマイクロタスクを処理する
マイクロタスクキューでの「コンソールL」を処理 → L
現時点
タスクキュー:「コンソールB及び中のすべてタスク」、「コンソールI及び中のすべてタスク」
マイクロタスクキュー:
-----------Round3-------------
タイマになったら、タスクキューでのタイマタスクを処理する(FIFO)
「コンソールB及び中のすべてタスク」
タスク「コンソールB」を処理、
Promiseのマイクロタスク「コンソールC」、「コンソールD」が順番にマイクロタスクキューに追加される、
タイマのタスク「コンソールE及び中のすべてタスク」がタスクキューに追加される、
現時点
タスクキュー:「コンソールI及び中のすべてタスク」、「コンソールE及び中のすべてタスク」
マイクロタスクキュー:「コンソールC」、「コンソールD」
-----------Round4-------------
単一タスクが終わったら、すべてのマイクロタスクを処理する
マイクロタスクキューでマイクロタスクを処理する(FIFO)
「コンソールC」を処理 → C
「コンソールD」を処理 → D
現時点
タスクキュー:「コンソールI及び中のすべてタスク」、「コンソールE及び中のすべてタスク」
マイクロタスクキュー:
-----------Round5-------------
タイマになったら、タスクキューでのタイマタスクを処理する(FIFO)
「コンソールI及び中のすべてタスク」
タスク「コンソールI」を処理、
Promiseのマイクロタスク「コンソールJ」、「コンソールK」が順番にマイクロタスクキューに追加される
現時点
タスクキュー:「コンソールE及び中のすべてタスク」
マイクロタスクキュー:「コンソールJ」、「コンソールK」
-----------Round6-------------
単一タスクが終わったら、すべてのマイクロタスクを処理する
マイクロタスクキューでマイクロタスクを処理する(FIFO)
「コンソールJ」を処理 → J
「コンソールK」を処理 → K
現時点
タスクキュー:「コンソールE及び中のすべてタスク」
マイクロタスクキュー:
-----------Round7-------------
タイマになったら、タスクキューでのタイマタスクを処理する(FIFO)
「コンソールE及び中のすべてタスク」
タスク「コンソールE」を処理、
Promiseのマイクロタスク「コンソールF」、「コンソールH」が順番にマイクロタスクキューに追加される
現時点
タスクキュー:
マイクロタスクキュー:「コンソールF」、「コンソールH」
-----------Round8-------------
単一タスクが終わったら、すべてのマイクロタスクを処理する
マイクロタスクキューでマイクロタスクを処理する(FIFO)
「コンソールF」を処理 → F
「コンソールH」を処理 → H
現時点
タスクキュー:
マイクロタスクキュー:「コンソールF」、「コンソールH」
------------------------------
*/
非同期処理を全部理解するまでは、まだまだ色々なヒントを揃わないといけないですが、もし少しても非同期処理に興味が有れば嬉しいです。
本記事はこちらのブログを勉強して途中のまとめですが、これからも引き続き勉強して感想をまとめようと思っています。
FYI
明日は、@zushi_ryotaさんが、Mockito-Kotlinを使ってテストコードを書いてみる!ぜひ、お楽しみください〜