【JavaScript】非同期処理のasync/awaitはPromiseの書き換えということを理解しておきたい

 

こんにちは。なんだか久々の投稿となってしまいました。

JavaScriptを使用してコードを書いていると、ほとんどの場合で利用することになる、非同期処理についての記事となります。

非同期処理の1つであるPromiseについて理解した上で、それを簡潔に書けるasync/await構文の紹介もしています。

JavaScriptを始めたてですと、このあたりでつまづくこともあるかもしれませんが、何となくでコードを書かないようにしっかりと、async/awaitはPromiseの書き換えということを理解して行きましょう。

 

 

JavaScriptでのPromise

JavaScriptでは非同期処理をPromiseを使って書くことができます。その定義・呼び出し方法を見ていきましょう。

 

定義

Promiseを定義する基本形は次のようになっています。

function asynchronous() {
  return new Promise((resolve, reject) => {
    // 処理...
    const val = 1;
    resolve(val); // または reject(err);
  });
}

Promiseコンストラクタにコールバックを渡すような形になっております。

コールバックの引数である、resolve(), reject() のいずれかが呼ばれた時点でPromiseの状態が確定し、呼び出し元で受け取れるようになります。

 

コールバックの説明としては、

  • resolve, reject を引数とする
  • resolve: Promise が成功したとみなされる際に呼び出す。resolve(1); のように値を返すことも可能。(イメージとしては return みたいなもの)
  • reject: Promise が失敗したとみなされる際に呼び出す。reject(new Error(reason)); のようにエラーオブジェクトを渡すことが多い。 (イメージとしては throw みたいなもの)

 

呼び出し

上記のように定義された関数を普通に呼び出してしまうとPromiseオブジェクトが返されてしまいます。

console.log(asynchronous());
// Promise {<pending>} または Promise {<fulfilled>: 1}

ということは、このままでは使い物になりません。

なので、.then, .catch という Promiseオブジェクトのメソッドを利用して処理をしてあげる必要があります。

asynchronous().then(val => {
  // resolve(val); されたときの処理
  console.log(val); // 1
}).catch(err => {
  // reject(err); されたときの処理
  console.log(err);
});

これで無事外側から resove(); reject() で返却された内容を取得できるわけです。

 

ちなみにPromiseで定義されている関数かわからないという場合に、Visual Studio Codeならば、マウスオーバーで簡単に知ることができます。

Promise関数にマウスオーバー

(コロン(:)の先がPromise<>となっているかどうか)

 

複数Promiseの扱い

 

では次に、複数Promise関数の連続呼び出しにどう対応するのかということに触れておきます。

ここでは、Promiseを返す以下の3つのfunctionがあると想定します。

function asynchronous1() {
  return new Promise((resolve, reject) => {
    resolve(1);
  });
}
function asynchronous2() {
  return new Promise((resolve, reject) => {
    resolve(2);
  });
}
function asynchronous3() {
  return new Promise((resolve, reject) => {
    resolve(3);
  });
}

 

ネスト式

まずは例を、

asynchronous1().then(val1 => {
  // val1 が使える
  asynchronous2().then(val2 => {
    // val1, val2 が使える
    asynchronous3().then(val3 => {
      // val1, val2, val3 が使える
      console.log(val1, val2, val3);
      // 1, 2, 3
    }).catch(err => {
      console.log(err);
    });
  }).catch(err => {
    console.log(err);
  });
}).catch(err => {
  console.log(err);
});

普通のやり方ですがコード量とネストが多くなってしまう反面、3つすべての返り値にアクセスできます。

 

メソッドチェーン式

asynchronous1().then(val1 => {
  // val1 が使える
  return asynchronous2();
}).then(val2 => {
  // val2 が使える
  return asynchronous3();
}).then(val3 => {
  // val3 が使える
  console.log(val3);
}).catch(err => {
  console.log(err);
});

.then が新しい Promise オブジェクトを返すので、メソッドチェーンを作ることができます。途中のエラーはすべて1つの.catch で対応できます。

コード量とネストは少ないですが前のthenの引数にアクセスするために一工夫必要になってしまいます。(ここでは言及しません)

 

その他

その他、Promise.all() の利用などもありますが、ここでは順番に呼び出すことだけを考えているので省略します。

 

async/awaitに書き換える

 

さて、比較的新しい書き方の async/await について、見ていきましょう。

文法的には async は上記 Promise定義の書き換えであり、await は上記Promise呼び出しの書き換えとなっています。

更に、必ず書き換えられるわけではなく、Promiseの使用しなくてはいけない場面も存在します。

なので、Promiseが理解できていない場合は、そちらを先に理解することを強く推奨します。

 

async

async を利用することで、Promise定義を以下のような書き換えができます。

  1. return new Promise((resolve, reject) => {}); の記述を省略できる
  2. resolvereturnrejectthrow に書き換えられる

 

では実際に見てみます。上記の定義の書き換えをしてみます。

async function asynchronous() {
  // 処理...
  const val = 1;
  return val; // または throw err;
}

こんな感じにかなりスッキリさせられます。

 

await

await を利用することで、Promise呼び出しを以下のような書き換えができます。

  1. .then で受け取っていた引数は await することで、直接受け取れる
  2. .catch で受け取っていたエラーは try-catch ブロックで処理できるようになる
  3. ただし、await 演算子は async で定義されている関数の中でしか使えない

 

ではこちらも実際に見てみます。上記の呼び出しの書き換えをしてみます。async で定義されている関数の処理として見てください。

try {
  const val = await asynchronous();
  // resolve(val); されたときの処理
  console.log(val); // 1
} catch (err) {
  // reject(err); されたときの処理
  console.log(err);
}

これだけですと、あまり利点はないようですが、複数呼び出しのパターンではかなりコード量削減ができます。

試しに上記ネスト式を書き換えてみましょう。(上記メソッドチェーン式 の書き換えも同じようになるはず)

try {
  const val1 = await asynchronous1();
  // val1 が使える
  const val2 = await asynchronous2();
  // val1, val2 が使える
  const val3 = await asynchronous3();
  // val1, val2, val3 が使える
  console.log(val1, val2, val3);
  // 1, 2, 3
} catch (err) {
  console.log(err);
}

コード量が多いというデメリットを解消しつつ、前の引数にも簡単にアクセスできます。

 

JavaScriptのその他の非同期処理の実現

 

Promiseでの非同期の実現以外にJavaScriptではいくつかの非同期の実現方法があります。

  • コールバックを利用する (setTimeoutでn秒後に処理を行うなど)
  • イベントを利用する (FileReaderでファイルを読み込むときなど)

ただ、これらの方法はうまくやることでPromiseに書き換える(ラップする)ことができることもあり、更にそれをasync 関数から await で呼び出すことで、コードを簡潔にすることができます。

なので、async/await を理解する = Promiseを理解することはJavaScriptでは大事なのです。

 

まとめ

Promise は JavaScriptを開発する上でほぼ必須と言っていいものなので、曖昧にせずしっかりと理解していきましょう。

 

以下のmozillaのドキュメントも合わせて見ておくとより理解が深まると思います。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises