はじめに

久しぶりに JavaScript / TypeScript に触れる機会がありました。
JavaScript の非同期処理は思い出すのにいつも時間がかかります。今後のために非同期処理について少し詳しくまとめます。

本記事では、サンプルコードとともに Event Loop の仕組み、Microtasks / Macrotasks の実行タイミングを整理しながら非同期処理についての理解を深めることを目的にしています。

本記事はおもに In depth: Microtasks and the JavaScript runtime environment を参考にしています。

Event Loop とは

JavaScript の Event Loop は、非同期処理の「実行タイミング」を制御する仕組みです。
JavaScript は(同じ Event Loop のサイクルにおいて)同期処理をすべて実行した後に非同期処理を実行します。言い換えると、非同期処理の準備が整っていても(解決済みでも)同期処理に割り込むことはありません。

  • 同期処理 -> 即時実行される
  • 非同期処理 -> キューに登録され、順番に実行される

非同期処理の種類(タスクの分類)

In depth: Microtasks and the JavaScript runtime environment では非同期処理を 2 種類の「キュー(待ち行列)」に分類して説明しています。

種類 名前 実行タイミング
Microtasks マイクロタスク 現在のタスクの後すぐ実行 Promise.then, queueMicrotask
Macrotasks タスク 次の Event Loop のタイミングで実行 setTimeout, setInterval

注意点

  1. 「現在のタスク(current task)」という表現が MDN にもありますが、これは setTimeout のような「Macrotasks の 1 件」だけを指すわけではありません。文脈によっては、「今 JavaScript が同期的に実行している 1 サイクル全体(=現在の Event Loop のサイクル)」を指している場合もあります。文脈に注意して解釈する必要があります。
  2. Macrotasks は Microtasks との対比を強調する意味で使用されることが多いようです。[In depth: Microtasks and the JavaScript runtime environment] は単に MacrotasksTasks と表記しています。本記事は分かりやすさのために Microtasks / Macrotasks で統一します。

サンプル

今までの説明をサンプルコードを使って見ていきます。

サンプル1:基本的な実行タイミングを確認

コード

// (1)..(5) は実行順序を表します

function sampleAsynchronous() {
  return new Promise(resolve => {
      console.log('task 1.');  // (1)
      resolve('task 4.');
  });
}

const promiseObj = sampleAsynchronous();

setTimeout(() => {
  console.log('task 5.');      // (5)
}, 0);

console.log('task 2.');        // (2)

promiseObj.then(data => {
  console.log(data);           // (4)
});

console.log('task 3.');        // (3)

// 出力結果

task 1.
task 2.
task 3.
task 4.
task 5.

解説

以下の (1)…(5) はコードのコメントに記載 (1)…(5) に対応します

  1. 同期処理 (1)〜(3):すぐに実行される
  2. Microtasks (4):Promise.then は Microtasks として登録され、同期処理が終わった後に即実行される
  3. Macrotasks (5):setTimeout は次の Event Loop サイクルで実行される

Event Loop のサイクル構造(簡略図)

 ┌────────────┐
 │ Macrotasks │ <- (1回目: 同期処理)
 └────┬───────┘
      ↓
 ┌────────────┐
 │ Microtasks │ <- (Promise.thenなど)
 └────┬───────┘
      ↓
   次の Macrotasks(setTimeout など)

サンプル2:複数サイクル実行タイミングを確認

// (1)..(10) は実行順序を表します

function createPromise() {
  return new Promise(resolve => {
    console.log('task 2.');  // (2)
    resolve('task 4.');
  });
}

function createOtherPromise() {
  return new Promise(resolve => {
    console.log('task 6.');  // (6)
    resolve('task 8.');
  });
}

console.log('task 1.');      // (1)

const promiseObj = createPromise();
promiseObj.then(data => console.log(data));  // (4)

setTimeout(() => {
  console.log('task 5.');    // (5)

  const other = createOtherPromise();
  other.then(data => console.log(data));     // (8)

  setTimeout(() => {
    console.log('task 9.');  // (9)
    console.log('task 10.'); // (10)
  }, 2000);

  console.log('task 7.');    // (7)
}, 1000);

console.log('task 3.');      // (3)
// 出力結果(順序)

task 1.
task 2.
task 3.
task 4.
task 5.
task 6.
task 7.
task 8.
task 9.
task 10.

解説

  • 1回目の Event Loop
    • Macrotasks:task 1. -> task 2. -> task 3.
    • Microtasks:task 4.(Promise.then)
  • 2回目の Event Loop( 1000ms 後の setTimeout )
    • Macrotasks:task 5. -> task 6. -> task 7.
    • Microtasks:task 8.( Promise.then )
  • 3回目の Event Loop( さらに 2000ms 後の setTimeout )
    • Macrotasks:task 9. -> task 10.
    • Microtasks:なし

まとめ

まとめとしてに本記事の要点を記載します。

  • Promise.then は同期処理が終わった直後に実行される( Microtasks )
  • setTimeout は 次の Event Loop サイクルで実行される( Macrotasks )

Event Loop は Macrotasks( current task ) -> すべての Microtasks → 次の Macrotasks( Tasks ) という流れで進行します。

非同期処理は JavaScript の基本的な機能ですが、文脈による言葉の使い分け(特に「task」)も含め、分かりづらい面もあります。 本記事が非同期処理の理解の一助になれば幸いです。