JavaScriptのconcurrency model(同時実行モデル)非同期処理について

2021年3月15日JavaScript

JavaScriptのconcurrency model(同時実行モデル)について、

コールスタックで処理できる実行コンテキストは一つずつにも関わらず、非同期の処理について簡単にまとめました。

JavaScriptのconcurrency model(同時実行モデル)について

非同期の処理はWeb APIで行われている

非同期の処理はバックグランドで行われていると言われることがありますが、そのバックグランドというのが、ブラウザのWeb APIsのことです。
つまり、fetch()もsetTimeout()もDOM()も、すべての非同期の処理は、ブラウザのWeb APIの中で処理が実行されています。

くるくる回るローディングイメージなどもWeb APIで実行されています。仮にローディングイメージをコールスタックで動かすとその下で待ち構える実行コンテキストが実行されずにすべて止まってしまいます。

Web APIからコールバックキューへ

コールバックキューとは、実行待ちの非同期処理の行列のことです。
つまり、非同期処理の実行順を管理しているところです。
先入れ先出しでタスクを実行するので、FIFO(First In, First Out)といいます。
たとえば、画像のロードが完了するまではWeb APIにいます。画像のロードがWeb APIで完了したら、ロードのイベントが発生して、コールバックキュー(callback queue)に移動します。
コールバックキューは、TODOリストのようなもので、タスクキューとも呼ばれます。
コールバックキューでは、それぞれのコールバックが実行されるのを待っています。

コールバックキューからコールスタックへ

コールバックキューの中にあるタスクを実行していくのがコールスタックとなります。
コールバックキューとコールスタックの間でイベントをどんどん回していくことをイベントループといいます。

イベントループとイベントループティックについて

イベントループで起こっていることは次のとおりです。
コールバックキューは、コールスタックに実行コンテキストがない状態になったら、そのコールバックをコールスタックに移します。これをイベントループティック(event loop tick)といいます。
このイベントループがそれぞれのコールバックをいつ実行するのかを決めています。JavaScriptエンジンには非同期の処理はありませんので、渡されたタスクをひたすら実行していきます。

マクロタスク(Macrotasks)とマイクロタスク(Microtasks)

マクロタスク(Macrotasks)とは、コールバックキュー(タスクキュー)のことです。
マイクロタスク(Microtasks)とは、タスクキューとは別で存在する非同期処理の待ち行列のことです。ジョブキューとも呼ばれます。Promiseで処理された非同期処理が代表例です。
どちらも非同期処理の待ち行列を管理しているキューです。
マイクロタスクは、順番が回ってきたら、すべてのジョブが実行されます。
マクロタスクは、一つずつのタスクを実行します。

マイクロタスクの代表的な例は、

  • Promise
  • queueMicrotask
  • MutationObserver

などがあります。

マクロタスクの代表的な例は、

  • setTimeout()
  • setInterval()
  • requestAnimationFrame()

などがあります。

Promiseのマイクロタスクキューについて

Promiseは、マイクロタスク(Microtasks)の代表格です。
fetchなどの非同期の処理が終わった後、マイクロタスクは、コールバックキューに移動するのではなく、マイクロタスクキューに移動します。
マイクロタスクキューは、コールバックキューよりも優先されるされて実行されます。
イベントループはまずマイクロタスクキューにタスクがないかを確認して、それらをすべてコールスタックに移動させてから、コールバックキューへタスクがないかを確認します。
重要なことは、マイクロタスクキューのタスクは、常に通常のコールバックキューのタスクよりも優先されるという点です。
次のコードを実行してみると、表示順でマイクロタスクキューが優先されているかがわかります。


console.log('テスト開始: 表示順1番目');
setTimeout(() => console.log('0秒タイマーの結果: 表示順4番目'), 0);
Promise.resolve('Promiseのresolve1の結果: 表示順3番目').then(response => console.log(response));
console.log('テスト終了: 表示順2番目');
// テスト開始 表示順1番目
// テスト終了 表示順2番目
// Promiseのresolve1の結果 表示順3番目
// 0秒タイマーの結果 表示順4番目

ただし、次のようにマクロタスクの中でresolve()を実行することで、then()メソッドというマイクロタスクを後で実行することもできます。

new Promise(function promise(resolve) {
  console.log('1番目');

  setTimeout(function third() {
    console.log('3番目');
    resolve();
  });
}).then(function forth() {
  console.log('4番目');
});
console.log('2番目');

// 1番目
// 2番目
// 3番目
// 4番目