• トップ
  • ブログ一覧
  • 【JavaScript】Promise で非同期処理を記述する
  • 【JavaScript】Promise で非同期処理を記述する

    広告メディア事業部広告メディア事業部
    2019.05.22

    IT技術

    はじめに~同期処理と非同期処理について~

    本記事では JavaScript の非同期処理を扱うための Promiseという仕組みについて取り上げます。

    JavaScript、とくにサーバーサイドで利用されるNode.js では非同期処理を利用するAPIが多数出てきます。

    Promise の仕組みを理解しておくとプログラムの作成がはかどるはずです。

    まず本題へ入る前に『同期処理』『非同期処理』について説明させていただきます。

    JavaScript の同期処理

    同期処理とは上から順番に処理されていくことです。

    処理が終了するのを待ってから、次の処理を実行します。

    1console.log(1);
    2console.log(2);
    3console.log(3);

    [結果]
    1
    2
    3

    JavaScript の非同期処理

    非同期処理とは、1つの処理が終了するのを待たずに、次の処理を実行することです。

    終了を待たないので処理を終了する順番が、上下することがあります。

    ファイルの読み込み、ネットワークの通信処理、データベースアクセスなどは非同期処理で行うことが多いです。

    なぜ非同期処理は必要なの?

    同期処理は上から順番に実行してくれますし、感覚的にもわかりやすいです。

    ですが、同期処理だと時間のかかってしまう処理が終了するまでは次の処理を行うことができません。

    たとえばサーバーと通信するとき、サーバーから応答が帰ってくるのに数秒かかる時があります。

    その間、処理が止まっていては問題です。

    その対策として、非同期処理があります。

    非同期処理を用いた例

    非同期関数のsetTimeout(function callback() {} , delay)は指定時間(ms) だけ処理を遅らせてから、callback 関数を実行します。

    今回は callback 関数に console.log(2) を指定しました

    1console.log(1);
    2setTimeout(function() {
    3  console.log(2);
    4}, 1000);
    5console.log(3);

    [結果]
    1
    3
    2

    console.log(2) が呼ばれる前に、console.log(3)が呼ばれます。

    setTimeout()は非同期関数のため、処理が止まることもなく続行されたためです。

    Promise とは

    前置きが長くなりましたが、ここからが本題です。

    Promise とは、非同期処理の状態をあわらすオブジェクトです。

    Promise の構文は以下のようなものです。

    1new Promise( /* executor */ function(resolve, reject) { ... } );

    Promise 関数の対応状況

    Chrome, Firefox, Edge, Safariといった最新のブラウザでは標準で対応しています。

    IE 11 は対応していないので、 polyfill で対応する必要があります。

    こんな方にオススメの記事

    1. JavaScript を学習中の方
    2. 非同期処理、同期処理がいまいちわからない方
    3. Promise を理解したい方

    Promise の利用

    Promise を利用する場合、関数の returnnew Promise() を指定し、コールバック関数を Promise に登録します。

    コールバック関数には、処理が成功したときには resolveを呼び、処理が失敗したときは rejectを呼ぶように記述します。

    1function promiseFunc(pay) {
    2  // Promise を返す
    3  return new Promise(function(resolve, reject) {
    4      if ( /*関数の成功条件*/ ) {
    5    //   成功したとき
    6        resolve()
    7      } else {
    8    //   失敗したとき
    9        reject()
    10      }
    11  });
    12}

    注目してほしいのは以下の箇所です。

    1  return new Promise((resolve, reject) => { ... }

    関数の returnnew Promise(function()) と記述しています。Promise インスタンスを返却するように記述すると、Promiseが利用できます

    たとえば、購入処理に約0.5秒かかってしまうbuy()関数なんてものを作ろうと思ったら、このような記述になります。

    お金が足りていたら、お釣りの金額を引数としたresolve(/*お釣り*/)を呼び、お金が足りていなければreject(/*メッセージ*/)を呼ぶ関数としています。

    1function buy(pay) {
    2  // Promise を返す
    3  return new Promise(function(resolve, reject) {
    4    setTimeout(function() {
    5      if (pay >= 100) {
    6        console.log("100円で購入しました");
    7        // 成功時
    8        resolve(pay - 100);
    9      } else {
    10        // 失敗時
    11        reject("お金が足りないよ");
    12      }
    13    }, 500);
    14  });
    15}

    Promise の状態

    さて、さきほどPromise とは状態を表すオブジェクトといいました。

    具体的には以下の状態があります。

    pending:初期状態、実行中。成功も失敗もしていない。
    fulfilled:処理が成功した状態。
    rejected:処理が失敗した状態。

    1. resolve() が呼ばれると、fulfilledの状態になります。
    2. rejected() が呼ばれると、rejectedの状態になります。

    Promise の then()

    Promise の処理が終了したのち、結果を取得するにはthen()を利用します。

    then() は、 Promise のインスタンスの状態がfulfilled となったときに実行する関数を登録できるインスタンスメソッドです。

    以下のような構文となっています。

    1promise.then(onFlufilled, onRejected)

    onFulfilled : fulfilled の状態のとき(resolveが呼ばれたとき)に実行される関数
    onRejected : rejected の状態のとき(rejectが呼ばれたとき)に実行される関数

    resolve(/*引数*/) の引数部分が、onFulfilledonRejected のところで登録する関数の引数となります。

    たとえば先程のbuy関数のお釣りを引数として受け取るには、以下のようにします。

    1// 関数定義
    2function buy(pay) {
    3  return new Promise(function(resolve, reject) {
    4    setTimeout(function() {
    5      if (pay >= 100) {
    6        console.log("100円の商品を購入しました");
    7        resolve(pay - 100);
    8      } else {
    9        reject("お金が足りないよ");
    10      }
    11    }, 500);
    12  });
    13}
    14
    15// 実行部分 と then 部分
    16console.log(1);
    17buy(300)
    18  .then(function(change) {
    19      console.log(`お釣りは${change}円です`)
    20      })
    21console.log(3);

    [結果]
    1
    3
    100円の商品を購入しました
    お釣りは200円です

    実行結果を確認すると、「お釣りは 200円です」と表示されており、resolve()の引数を受け取れていることが確認できるとわかります。

    さて、さらにここで注目していただきたいのは順番です。

    ソースコードの記述の順番と実行結果のメッセージの順番が異なっていることです。

    1console.log(1); // 1番目
    2buy(300) // buy は2番目に呼ばれている。
    3  .then(function(change) {
    4      console.log(`お釣りは${change}円です`)
    5      })
    6console.log(3); // 3番目

    [結果]
    1
    3
    100円の商品を購入しました #結果は3番目
    お釣りは200円です

    buyは、 Promiseを利用して非同期関数となっており、処理が終了するまでそこでコードの実行が止まることはないからです。

    console.log(1), console.log(3) は即時に実行されるますが、buy(300)の処理は約 0.5秒かかってしまうので、実行結果のような表示になります。

    Promise の catch()

    Promise のインスタンスのエラー処理には catch()を利用します。

    catch() とは Promise のインスタンスの状態がrejected となったときに実行する関数を登録するインスタンスメソッドです。

    それではあえて処理を失敗させて reject() を発生してみましょう。

    buy()の引数に 100以下を指定します。(簡略化のためにアロー関数を利用しています)

    1// 関数定義は省略
    2
    3// 100 以下を指定してエラーを表示させる。
    4buy(50)
    5  .then(change => console.log(`お釣りは${change}円です`))
    6  .catch((error) => console.error(error));

    [結果]
    お金が足りないよ

    then()が呼び出されずにcatch()だけが呼び出されてエラー処理されているのがわかります。

    このように、Promise を利用したコードは then()catch()で正常処理、エラー処理を切り替えることができます。

    Promise のメソッドチェーン

    このthen(),catch() はつなげて利用することもできます。

    つなげて利用する場合、Promise が終了するのを待ってから、次のthen() が呼ばれます。

    それでは、連続で商品を購入してみましょう。

    1function buy(pay) {
    2  return new Promise((resolve, reject) => {
    3    setTimeout(function() {
    4      if (pay >= 100) {
    5        console.log("100円の商品を購入しました");
    6        resolve(pay - 100);
    7      } else {
    8        reject(Error("error"));
    9      }
    10    }, 500);
    11  });
    12}
    13
    14buy(550)
    15  .then(change => {
    16    console.log(`お釣りは${change}円です`);
    17    return buy(change);
    18  })
    19  .then(change => {
    20    console.log(`お釣りは${change}円です`);
    21    return buy(change);
    22  })
    23  .then(change => {
    24    console.log(`お釣りは${change}円です`);
    25    return buy(change);
    26  })
    27  .then(change => {
    28    console.log(`お釣りは${change}円です`);
    29    return buy(change);
    30  })
    31  .then(change => {
    32    console.log(`お釣りは${change}円です`);
    33    return buy(change);
    34  })
    35  .then(change => {
    36    console.log(`お釣りは${change}円です`);
    37    return buy(change);
    38  })
    39  .then(change => {
    40    console.log(`お釣りは${change}円です`);
    41    return buy(change);
    42  })
    43  .catch(() => console.log("お金が足りないよ"));

    [結果]
    100円の商品を購入しました
    お釣りは450円です
    100円の商品を購入しました
    お釣りは350円です
    100円の商品を購入しました
    お釣りは250円です
    100円の商品を購入しました
    お釣りは150円です
    100円の商品を購入しました
    お釣りは50円です
    お金が足りないよ

    実行してみると約 0.5 秒間隔で文字が表示されていくと思います。

    前述しましたが、このようにPromise のthen()を利用することで、連続して非同期処理を記述できます。

    また、ソースコード中では buy()8回も呼んでいますが、購入処理が行われたのは 5回だけです。

    お金が足りなくなったので途中の buy() の中でreject() が呼び出されたからです。

    それ以降の then() の処理が行われることはなく、catch()でエラー処理されるからです。

    then () のなかの関数の return の値が Promise インスタンスじゃなかったときは?

    then() の引数として渡された関数がPromise を返すのではありません。

    値を返した場合は、Promise.resoleve(<関数が返した値>)が自動的に実行され Promise でラップされます。

    .catch() のあとの .then()

    Promise のメソッドチェーンで .catch()のあとに.then()でチェーンすることも可能です。

    これによってエラー処理が失敗したあとでも新しい動作を定義できます

    1asyncThing1()
    2  .then(function() {
    3    return asyncThing2();
    4  })
    5  .then(function() {
    6    return asyncThing3();
    7  })
    8  .catch(function(err) {
    9    return asyncRecovery1();
    10  })
    11  .then(
    12    function() {
    13      return asyncThing4();
    14    },
    15    function(err) {
    16      return asyncRecovery2();
    17    }
    18  )
    19  .catch(function(err) {
    20    console.log("Don't worry about it");
    21  })
    22  .then(function() {
    23    console.log("All done!");
    24  });

    フローチャート

    すこし複雑ですが、上記のようなメソッドチェーンのフローチャートは以下の図のようになります。

    実線が正常処理の流れ、点線が失敗時の流れです。

    図を見ると、asyncRecovery1 でエラー処理を行った後に、正常処理のasyncThing4 に戻るフローがあるとわかります。

    Promise で非同期処理の処理を包む

    Promise は便利ですが、古い API は Promiseのインターフェイスを提供しておらず、callback 関数のみの時があります。

    具体的な例としては setTimeout() が当てはまります。

    1setTimeout(func, 1000);

    setTimeout() を Promise に対応させてみましょう。

    1// setTimeout を Promise で包む
    2const wait = function(milliSeconds) {
    3  return new Promise(function(resolve) {
    4    setTimeout(resolve, milliSeconds);
    5  });
    6};
    7
    8// 省略バージョン
    9const wait = milliSeconds =>
    10  new Promise(resolve => setTimeout(resolve, milliSeconds));

    呼び出し方法

    1wait(1000).then(() => {
    2  console.log("hello");
    3});

    このように Promise で APIをラップしたのち、それ以降は二度と直接呼ばないようにするとよいです。

    さいごに

    以上、非同期処理の Promise についてでした。

    最後に、まとめて終わりにしたいと思います。

    1. Promise を利用することによって非同期処理を記述できます。
    2. 正常時にはresolve() を呼び、エラー時にはreject()を呼びます。
    3. then() で処理をつなげることができます。
    4. catch() でエラー処理を記述できます。

    Promiseの仕組みを理解しておくとプログラムの作成がはかどるので、ぜひ使いこなせるように勉強しましょう!

    こちらの記事もオススメ!

    featureImg2020.08.07JavaScript 特集知識編JavaScriptを使ってできることをわかりやすく解説!JavaScriptの歴史【紆余曲折を経たプログラミン...

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    GitHubActionsのランナーに触れてみた

    こやまん(エンジニア)

    こやまん(エンジニア)

    2024.03.28

    IT技術

    Azure Data FactoryでSlackへ通知をしてみる

    たかやん(エンジニア)

    たかやん(エンジニア)

    2024.03.28

    IT技術

    GCP Secret Managerを使ってみた

    たなゆー(エンジニア)

    たなゆー(エンジニア)

    2024.03.21

    IT技術

    Bitriseのパイプラインと環境変数

    加納(エンジニア)

    加納(エンジニア)

    2024.03.11

    IT技術