Skip to content
Go back

(備忘録)JavaScriptにおける非同期処理を学んだ

Published:  at  11:14 PM

JavaScript/TypeScriptにおける非同期処理を完全に理解したので、見返すようにまとめました。

(内容指摘・修正提案大歓迎です)

Table of contents

Open Table of contents

Javascriptのスレッド

JavaScriptはシングルスレッド上で実行される。 ブラウザには主に以下の3つのスレッドが存在し、JavaScriptが実行されるのは Main Thread。

同期処理と非同期処理

例) setTimeout(callback, ms)

setTimeoutを実行した時点でメインスレッドから切り離される。 そして非同期API(内部で実装されたタイマー)に渡されたのち、指定した時間後にコールバックがタスクキューに積まれる。

タスクキューとコールスタック

重要な登場人物一覧:

  1. コールスタック
  1. イベントループ
  1. タスクキュー

Promise

Promiseのコード例

new Promise(function (resolve, reject) {
  setTimeout(function () {
    console.log("Task done");
  });
  resolve("hello"); // Promiseをfulfilled状態にし、thenのハンドラをキューに登録
  // reject("fail"); // Promiseをrejected状態にし、catchのハンドラをキューに登録
})
  .then(function (data) {
    // resolveの引数が渡ってくる
    console.log(data); // -> "hello"
    return data + " morita"; // 次のthenに値を渡す(渡される型はPromise)
  })
  .then(function (data) {
    console.log(data); // -> "hello morita"
    throw new Error("fail"); // このエラーは次のcatchに渡される
  })
  .catch(function (error) {
    // rejectの引数、またはthrowされたエラーが渡ってくる
    console.error(error);
  })
  .finally(function () {
    console.log("Promise処理終了(成功・失敗問わず)");
  });

console.log("Global context end");

MicroTasks と MacroTasks

キューには2種類ある。

Macro Tasks (タスクキュー)

Micro Tasks (ジョブキュー)

コードの実行順序

以下のコードを実行した時の実行順は?

console.log("starting");

new Promise(function (resolve) {
  setTimeout(function () {
    console.log("task1");
  });
  resolve();
})
  .then(function () {
    console.log("job1");
  })
  .then(function () {
    console.log("job2");
  });

console.log("Global context end");

答え:

starting
global context end
job1
job2
task1

処理の流れ:

await/async

コード例

const fetchCoffee = async (): Promise<void> => {
  const response = await fetch("https://api.sampleapis.com/coffee/hot");
  const coffeeList = await response.json();
  console.log(coffeeList);
};

fetchCoffee();

Promiseの静的メソッドを使った並行処理

Promiseの並行処理に関する静的メソッド一覧

1. Promise.all

引数にはPromiseを格納した反復可能オブジェクトを入れる。 全てのPromiseが”fullfiled”となった場合のみ成功扱い。 1つでも”rejected”となると、失敗となる。

返り値は、以下のようなPromiseを1つ返す。

// 成功時
Promise { <state>: "fulfilled", <value>: Array[5] }

// 失敗時
Promise { <state>: "rejected", <reason>: 5 }

2. Promise.race

最も早く”pending”状態が終わったPromiseを返す。 そのため、返ってくるPromiseは”fullfiled”か”rejected”のどちらかわからない。 また、どのPromiseも解決してない場合は”pending”となる

返り値の例:

// 成功時
Promise { status: 'fulfilled', value: 100 }

// 失敗時
Promise { status: 'rejected', reason: 300 }

// 未解決時
Promise { status: 'pending' }

3. Promise.allSettled

全てのPromiseが解決する(“pending”状態から抜ける)とそれら全てを返す。

返り値の例:

[
   { status: 'fulfilled', value: 33 },
   { status: 'fulfilled', value: 66 },
   { status: 'rejected', reason: Error: an error }
]

Promise.allを使用した並行処理の実例

リストの要素を並行処理でインクリメントする

const incNums = async (nums: number[]): Promise<number[]> => {
  return Promise.all(nums.map(n => n + 1))
    .then((result: number[]) => {
      if (result.length === 0) {
        throw new Error("failed to increment all nums");
      }
      return result;
    })
    .catch(err => {
      console.error(err);
      throw err;
    });
};

const nums = [1, 2, 3, 4, 5];
console.log(nums);
incNums(nums).then((result: number[]) => {
  console.log(result);
});

// ==== 出力 ====
// [ 1, 2, 3, 4, 5 ]
// [ 2, 3, 4, 5, 6 ]

setTimeoutを応用した定期実行

setTimeoutを使用することで定期実行を行う処理を実現することができる。

setIntervalではなく、setTimeoutを使うことで安全に定期実行を行うことができる。

setIntervalは一定時間間隔でタスクキューにタスクを積むため、前のタスクに時間がかかってしまっても、 止めない限りお構いなしにタスクを積んでいってしまう。そのためキューが詰まってしまうバグに繋がる。

一方で、setTimeoutを使用した場合、タスクの実行を待ってから、次のタスクを積むと言った具合に、 柔軟なスケジューリングが実装できる。

以下は、指定した回数分コンソールに数字を出力する処理をsetTimeoutで実装した。 (この程度の処理ならsetIntervalでもキューが詰まることはなさそうだが) コメントを記している箇所で、万が一時間のかかる処理が合ったとしても、その実行を待ってからタスクを積むため、 キューが詰まるリスクを回避することができる。

const count = (num: number): void => {
  let count = 0;
  let timer: ReturnType<typeof setTimeout>;

  const timerFunc = () => {
    if (count >= num) {
      clearTimeout(timer);
      return;
    }

    // 時間がかかる処理

    count++;
    console.log(count);
    timer = setTimeout(timerFunc, 1000);
  };

  timer = setTimeout(timerFunc, 1000);
};

修正を提案する