Michi's Tech Blog

一人前のWebエンジニアを目指して

JavaScriptの非同期処理についてまとめてみた②

こんにちは!
スマレジ テックファームのMichiです!

この記事は、『JavaScriptの非同期処理についてまとめてみた①』の続きになります。

前回のおさらい

さて、前回はsetTimeout関数を利用して、「5、4、3、2、1、0」のように1秒ずつカウントダウンする処理を作った結果、地獄のようなコードが生まれたのでした。

console.log('開始')
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(4);
        setTimeout(() => {
            console.log(3);
            setTimeout(() => {
                console.log(2);
                setTimeout(() => {
                    console.log(1);
                    setTimeout(() => {
                        console.log(0);
                    }, 1000);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

これを「コールバック地獄」と呼ぶのでしたね。 そして、このコールバック地獄を解決する方法が、Promiseasync/awaitだというところまで、前回の記事で書きました。
今回は、Promiseasync/awaitの具体的な使い方を解説していきたいと思います。

Promise

完成形

小難しい話をしてもイメージが湧かないと思うので、まずは完成形からお見せします。

/* 非同期処理を関数として定義 ...  ① */
const countDown = (num) => {
  // Promiseオブジェクトを生成して、戻り値として返す ... ②
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof num === "number") {
        console.log(num)
        resolve(); // 処理の成功を通知する ... ③
      } else {
        reject("整数を入力してください"); // 処理の失敗を通知する ... ③'
      }
    }, 1000);
  });
};

/* 実行 */
console.log('開始');
countDown(5)
  // 処理に成功した時、次のcountDownを呼び出す ... ④
  .then(() => countDown(4))
  .then(() => countDown(3))
  .then(() => countDown(2))
  .then(() => countDown(1))
  .then(
    () => countDown(0), 
     // 処理に失敗した時、エラーメッセージを出力 ... ④'
    (error) => console.log(error)
  );

Promiseを使用すると、first().then(second).then(third)...というように、thenで繋げることによって1本道のコードで記述できるようになります。直観的でわかりやすいコードになりましたね!
では、Promiseの使い方を解説します。

解説

Promiseを利用するには、まず非同期処理を関数としてまとめておきます(①)。
countDown関数が戻り値として返すのが、Promiseオブジェクトです(②)。

Promiseコンストラクター

new Promise((resolve, reject) => { statements })
    resolve: 処理の成功を通知するための関数
    reject: 処理の失敗を通知するための関数
    statement: 処理本体

引数のresolve/rejectは、それぞれ非同期処理の成功と失敗を通知するための関数です。
成功時はresolveメソッドを、失敗時はrejectメソッドを呼び出し、結果をthenに引き渡します。例では、成功時はコンソールに値を表示してから処理成功の通知を行っています(③)。失敗時はrejectの引数にエラーメッセージを渡すことで、処理失敗とエラーメッセージの通知を行います(③')。

resolve/rejectの通知内容を受け取るのは、thenメソッドの役割です。

thenメソッド

promise.then(success, failure)
    promise: Promiseオブジェクト
    success: 成功コールバック関数(resolve関数によって呼び出し)
    failure: 失敗コールバック関数(reject関数によって呼び出し)

例では、非同期処理に成功したとき、次のcountDownを呼び出しています(④)。失敗時はrejectから受け取ったエラーメッセージをコンソールへ出力します(④')。

ちなみに、countDown(5)countDown(1)の中では、失敗時のコールバック関数を渡していませんが、その場合、該当のthenメソッドはスキップされて、最後(countDown(0))の失敗コールバックが呼び出されます。このように、thenメソッド単位に失敗コールバックを定義せずに、必要な箇所でまとめてエラー処理できるのも、Promiseのいいところです。

でも、まだちょっと分かりにくくない…?

最初のコールバック地獄よりはだいぶマシになりましたが、Promiseは書き方が特殊でまだちょっと分かりにくいような気もします。 そこで登場するのが、ES2017から導入されたasync/awaitという書き方です。

async/await

完成形

これも先に完成形からお見せしましょう。

/* 非同期処理を関数として定義 ...  ① */
const countDown = (num) => {
  // Promiseオブジェクトを生成して、戻り値として返す ... ②
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof num === "number") {
        console.log(num);
        resolve(); // 処理の成功を通知する ... ③
      } else {
        reject("整数を入力してください"); // 処理の失敗を通知する ... ③'
      }
    }, 1000);
  });
};

/* 実行したい処理をasync関数の中で書く ... ④ */
const execute = async () => {
  console.log("開始");
  await countDown(5);
  await countDown(4);
  await countDown(3);
  await countDown(2);
  await countDown(1);
  await countDown(0);
};

/* 実行 */
execute();

awaitPromiseオブジェクトを同期的に処理する機能です。awaitを書くことによって、先ほどのthenのときより、より直感的に同期処理を行っているということが分かると思います。

解説

まず、awaitを使用するには条件があります。それは、asyncが付いた関数の中でしか利用できないということです(④)。
次に、同期処理を行いたいPromiseオブジェクトをasync関数の中で呼び出し、その前にawaitキーワードを付与します。awaitを付けることによって、Promiseが確定しその結果を返すまで、JavaScriptを待機させるという処理になります。 非常に簡単ですね!

ちなみに、async関数の中でreject(処理の失敗)を受け取りたい場合は、try/catchが使用できます。

  try {
    await countDown("hoge");
  } catch (e) {
    console.log(e);
  }

// 結果:整数を入力してください

まとめ

前回、今回に渡り、JavaScriptの非同期処理についてまとめました。
特にPromiseについては、JS初心者には分かりにくかったかもしれませんが、JSを扱う上で非常に大切な概念なので、しっかりマスターしておきましょう!