mosya

Promise入門 - 非同期処理の達人になろう

JavaScriptを極める上で鬼門になるのがPromiseです。Promiseとは非同期処理を便利に扱うための関数です。
まずは非同期処理がどういったものか抑えていきましょう。

非同期処理

非同期処理とはある関数の実行結果を待たず次の処理に進み、その関数の実行が終わったらそのタイミングであらかじめ定義しておいた関数(コールバック)を実行する処理を指します。
例えば以下の処理は3秒経過後にHelloを表示します。

setTimeout(function () {
  console.log("Hello");
}, 3000);
console.log("My name is Daigo");

このソースコードで、My name is DaigoHelloよりも先に出力されます。
setTimeoutで3秒経過するのを待たず次の行に処理が進んでいることがわかります。

JavaScriptはブラウザーで処理を止めてしまうと長い間画面が表示されなかったり訪れたユーザーに良い体験を与えないことから、長く時間がかかる処理はこのように非同期処理が用意されていることが多いです。
非同期処理には処理が終わった後実行される関数が用意されていることがありこの関数をコールバックといいます。setTimeoutでは第1引数の関数がコールバックにあたります。

このように一見便利そうな非同期処理ですが、多用するととてもソースコードが見づらくなることがあります。
例えば以下のソースコードをご覧ください。

setTimeout(function () {
  console.log("1秒経過");
  setTimeout(function () {
    console.log("2秒経過");
    setTimeout(function () {
      console.log("3秒経過");
      setTimeout(function () {
        console.log("4秒経過");
      }, 1000);
    }, 1000);
  }, 1000);
}, 1000);

このようにコールバックを使って秒数を測っていくと関数が入れ子になってしまいとても醜いコードになってしまいます。

Promiseの利用

ここで、Promiseの登場です。非同期処理をnew Promiseで囲って任意のタイミングでPromiseの第一引数であるresolveを実行してあげることで、
thenに引数として定義されているコールバックに処理が処理されます。

function wait(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

const promise = wait(1000);
promise.then(function () {
  // <= resolveの実行がこのコールバック関数のトリガーになる
});

Promiseを使って、先程のコールバックの入れ子を書き直してみましょう。
だいぶ処理がスッキリしました。

function wait(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

wait(1000)
  .then(function () {
    console.log("1秒経過");
    return wait(1000);
  })
  .then(function () {
    console.log("2秒経過");
    return wait(1000);
  })
  .then(function () {
    console.log("3秒経過");
    return wait(1000);
  })
  .then(function () {
    console.log("4秒経過");
    return wait(1000);
  });

reject

何らかの事情により処理が失敗した場合はPromiseの第二引数であるrejectを利用できます。rejectが実行されるとthenに処理が飛ぶことはなく代わりにcatchを使います。

例えば先程の例に、wait関数に0秒未満の数字が指定された場合はエラーを表示するようにしてみましょう。resolveの引数の次の引数にrejectを追加して、
以下のように0秒未満の数字が指定された場合はrejectを実行してみます。

この場合、1秒経過、2秒経過と表示された後、catchが実行され「0秒以上を指定してください」と表示されるのがわかります。

function wait(time: number) {
  return new Promise(
    (resolve, reject) => {
      if (time < 0) {
        reject(
          "0秒以上を指定してください"
        );
      }
      setTimeout(resolve, time);
    }
  );
}

wait(1000)
  .then(function () {
    console.log("1秒経過");
    return wait(1000);
  })
  .then(function () {
    console.log("2秒経過");
    return wait(-1000);
  })
  .then(function () {
    console.log("3秒経過");
    return wait(1000);
  })
  .then(function () {
    console.log("4秒経過");
    return wait(1000);
  })
  .catch((reason) => {
    console.error(reason);
  });

async awaitの利用

async, awaitを使うとさらにスッキリ処理を記述することができます。

通常の関数を非同期関数としてfunctionの前にasyncで宣言します。
async functionではresolveが実行されるまでawaitで待つことができ、処理が完了すると次の行に進みます。
1行ずつ処理が完結するのでとてもわかりやすいコードになります。

function wait(time: number) {
  return new Promise(
    (resolve, reject) => {
      if (time < 0) {
        reject(
          "0秒以上を指定してください"
        );
      }
      setTimeout(resolve, time);
    }
  );
}

async function main() {
  await wait(1000);
  console.log("1秒経過");
  await wait(1000);
  console.log("2秒経過");
  await wait(1000);
  console.log("3秒経過");
  await wait(1000);
  console.log("4秒経過");
}

main();

try catchの利用

また、Promise関数内で発生したrejectはasync function内ではtry catchで以下のようにエラーハンドリングをすることができます。

function wait(time: number) {
  return new Promise(
    (resolve, reject) => {
      if (time < 0) {
        reject(
          "0秒以上を指定してください"
        );
      }
      setTimeout(resolve, time);
    }
  );
}

async function main() {
  try {
    await wait(1000);
    console.log("1秒経過");
    await wait(1000);
    console.log("2秒経過");
    await wait(-1000);
    console.log("3秒経過");
    await wait(1000);
    console.log("4秒経過");
  } catch (e) {
    console.error(e);
  }
}

main();

先ほどと同じようにwait(-1000)が実行されるとtimeが0以下になるのでこのタイミングでrejectがよばれ処理がcatchに移ります。

処理の最適化にPromise.allを利用

最後に非同期処理が複数あるケースを考えてみましょう。全てawaitで待ってしまうと多くの時間を使ってしまう場合があります。
例えば以下の処理はユーザーA,ユーザーB,ユーザーC,ユーザーDそれぞれの情報を取得して、一番身長が高い順に並び替える処理です。

async function sortByHight() {
  const userA = await fetch(
    "/api/users/a"
  ).then((res) => res.json());
  const userB = await fetch(
    "/api/users/b"
  ).then((res) => res.json());
  const userC = await fetch(
    "/api/users/c"
  ).then((res) => res.json());
  const userD = await fetch(
    "/api/users/d"
  ).then((res) => res.json());

  const users = [
    userA,
    userB,
    userC,
    userD,
  ];

  return users.sort((a, b) => {
    if (a.height > b.height) {
      return 1;
    }
    return -1;
  });
}

ユーザーAが取得できてからユーザーBを取得し、さらにユーザーBが取得できてからユーザーCを取得しにいっているのでとても非効率で時間がかかってしまいます。
同時にユーザーA,ユーザーB,ユーザーC,ユーザーDを取得できると良さそうです。
そういったケースにPromise.allを使います。

async function sortByHight() {
  const users = await Promise.all([
    fetch("/api/users/a").then((res) =>
      res.json()
    ),
    fetch("/api/users/b").then((res) =>
      res.json()
    ),
    fetch("/api/users/c").then((res) =>
      res.json()
    ),
    fetch("/api/users/d").then((res) =>
      res.json()
    ),
  ]);

  return users.sort((a, b) => {
    if (a.height > b.height) {
      return 1;
    }
    return -1;
  });
}

Authored by

筆者の写真

Godai@steelydylan

Webサービスを作るのが好きなWebエンジニア。子供が産まれたことをきっかけに独立し法人化。サービス開発が大好き。
好きな言語はTypeScript。

ReactやTypeScriptなどの周辺技術が学べる
オンライン学習サービスを作りました!

詳しくはこちら
mosya

mosyaはオンラインでHTML,CSS,JavaScriptを基本から学習できるサービスです。現役エンジニアが作成した豊富なカリキュラムに沿って学習を進めましょう。

© 2023 - mosya. All rights reserved.