【STEP.21】
Promiseの使い方をマスターしよう!

Promiseとは

JavaScriptで非同期処理を実装する場合、古典的なアプローチの1つとしてコールバック関数を利用する方法があります。

たとえば、setTimeoutsetIntervalメソッド、XMLHttpRequestオブジェクトをはじめ様々なコールバック関数があります。

しかし、非同期処理がいくつも連なる場合、コールバック関数では入れ子が深くなりすぎて、1つの関数が肥大化する傾向にあります。

このような問題をコールバック地獄といいます。

first(function(data) {
  ...最初に実行すべき処理...
  second(function(data) {
    ...first関数が成功した時に実行すべき処理...
    third(function(data) {
      ...second関数が成功した時に実行すべき処理...
      fourth(function(data) {
        ...最終的に実行すべき処理...
      });
    });
  });
});

このような問題を解決するのが、Promiseオブジェクトの役割です。

Promiseオブジェクトを利用することで、上のようなコードを、あたかも同期処理のように1本道のコードで記述することができるようになります。

first().then(second).then(third).then(fourth);

ES2015でPromiseオブジェクトが標準的な非同期処理の仕組みとして導入されました。

fetchメソッドをはじめ、メジャーなライブラリ/フレームワークでもPromiseが前提となっている機能は多く、これらを利用する上でもPromiseの理解は欠かせません。

以下は、Promiseを利用したシンプルな非同期処理の例です。

数値が渡されると、500ミリ秒後に2倍にした値を、渡された値が数値でない場合にはエラーメッセージを、それぞれ返します。

function runAsync(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof value === 'number') {
        resolve(value * 2)
      } else {
        reject(new Error(`${value}は数値ではありません。`));	
      }
    },500);
  });
}
	
runAsync(15)
  .then(response => console.log(`成功[${response}]`))
  .catch(error => console.log(`失敗[${error}]`))
  .finally(() => console.log('終了'));
成功[30]
終了

まず、非同期処理を関数としてまとめます。

この例であれば、runAsyncがそれです。

関数は、戻り値としてPromiseオブジェクトを返すようにします。

Promiseコンストラクター

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

Promiseは非同期処理の状態を監視するためのオブジェクトです。

コンストラクターには非同期処理の本体(関数)を記述します。

引数resolverejectは、非同期処理の成功/失敗を通知するための関数です。

Promiseによって自動的に渡されるので、アプリ開発者はこれらを処理の結果に応じて、呼び出せば良いということです。

上のJavaScriptのコード4~8行目で、引数valueが数値であるかどうかを判定して、数値であればresolve(成功)、さもなければreject(失敗)を、それぞれ呼び出しています。

resolvereject関数には、それぞれ成功/失敗に伴う情報(たとえば処理の結果やエラーメッセージ)を、引数として渡せます。

resolvereject関数による通知を受け取るのは次に掲げる表のメソッドです。

【非同期処理の結果を受け取るためのメソッド】
メソッド概要
then成功した時の処理
catch失敗した時の処理
finally成功/失敗に関わらず、完了時の処理ES2018

thencatchメソッドのコールバック関数は、それぞれresolvereject関数から渡された値を受け取り、成功/失敗時の処理を実行します。

runAsync('Hoge')」のように文字列を渡した場合には、非同期処理が失敗して、以下のようなエラーが得られます。

失敗[Error: Hogeは数値ではありません。]
終了

複数の非同期処理を順に実行する【then】

thenメソッドを連結することで、複数の非同期処理を順に実行することもできます。

たとえば以下は、runAsync関数の成功を受けて、さらにrunAsync関数を呼び出す例です。(最終的に2*2*2倍の値が得られます。)

function runAsync(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof value === 'number') {
        resolve(value * 2)
      } else {
        reject(new Error(`${value}は数値ではありません。`));	
      }
    },500);
  });
}
	
runAsync(15)
  // 初回の実行に成功したら、2度目の実行
  .then(response => runAsync(response))
  // 2度目の実行に成功したら3度目の実行
  .then(response => runAsync(response))
  .then(response => console.log(`最終結果[${response}]`))
  .catch(error => console.log(`失敗[${error}]`))
最終結果[120]

非同期処理を連結するには、thenメソッド(成功コールバック)の配下で、新たなPromiseオブジェクトを返します。

この例であれば、初回のrunAsync関数の結果を受けて、さらにrunAsync関数を、その結果を受けてまたrunAsync関数を、というように、順に非同期処理を呼び出しています。

runAsync関数の戻り値はPromiseなので、これでthenメソッドをドット演算子(.)で列記できます。

もしも「runAsync('Hoge')」のようにrunAsync関数に数値以外の値を指定した場合には、2個目のthenメソッドはスキップされ、catchメソッド(失敗コールバック)が実行されます。

以下のような結果になります。

失敗[Error: Hogeは数値ではありません。]

複数の非同期処理を並行して実行する【Promise.all】

Promise.allメソッドを利用します。

allメソッド

Promise.all(proms)
proms    監視するPromiseオブジェクト(配列)

たとえば以下は、runAsync関数を複数同時に呼び出して、すべての処理が完了したところで、結果をまとめて出力する例です。

function runAsync(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof value === 'number') {
        resolve(value * 2)
      } else {
        reject(new Error(`${value}は数値ではありません。`));	
      }
    },500);
  });
}
	
Promise.all([
  runAsync(10),
  runAsync(15),
  runAsync(20),
])
.then(response => console.log(`成功[${response}]`))
.catch(error => console.log(`失敗[${error}]`));
成功[20,30,40]

allメソッドでは成功コールバックの引数response.then(response => console.log(`成功[${response}]`)))にも結果が配列として渡される点に注目です。

また、非同期処理のいずれかが失敗した場合には(他が成功したとしても)、成功コールバックは実行されず、失敗コールバック(.catch(error => console.log(`失敗[${error}]`));)だけが呼び出されます。

複数の非同期処理のどれかが成功したところで処理を実行する【Promise.race】

Promise.raceメソッドを利用します。

raceメソッド

Promise.race(proms)
proms    監視するPromiseオブジェクト(配列)

たとえば以下は、runAsync関数を複数同時に呼び出して、いずれかの処理が完了(または失敗)したところで、結果を表示する例です。

function runAsync(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof value === 'number') {
        resolve(value * 2)
      } else {
        reject(new Error(`${value}は数値ではありません。`));	
      }
    },500);
  });
}
	
Promise.race([
  runAsync(10),
  runAsync(15),
  runAsync(20),
])
  .then(response => console.log(`成功[${response}]`))
  .catch(error => console.log(`失敗[${error}]`));
成功[20]

結果は、最初に終了したものだけが報告されるので、どの処理が最初に終了したかによって、結果も変化する可能性があります。

Promiseの処理を同期的に記述する【async/await構文】

asyncawait構文を利用します。

たとえば以下は、runAsync関数の成功を受けて、数珠つなぎにrunAsync関数を呼び出していく例です。(最終的に2*2*2倍の値が得られます。)

function runAsync(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (typeof value === 'number') {
        resolve(value * 2)
      } else {
        reject(new Error(`${value}は数値ではありません。`));	
      }
   },500);
 });
}

async function multi(value) {
  let result1 = await runAsync(value);
  let result2 = await runAsync(result1);
  let result3 = await runAsync(result2);
  return result3;
}
	
multi(15)
  .then(response => console.log(`最終結果[${response}]`))
  .catch(error => console.log(`失敗[${error}]`));
最終結果[120]

まず、Promiseによる非同期処理をまとめた関数には、asyncキーワードを付与します。(ソースコード13行目)

これによって、関数は非同期関数(async function)と見なされるようになります。

そして、この非同期関数の中で利用できるのが、await演算子です。(ソースコード14~16行目)

非同期処理(=Promiseを返す処理)にawait演算子を利用することで、JavaScriptは非同期処理の終了を待って、待機します。

ただし、そのまま待機するわけではなく、「関数の残りの処理をプールしておき、呼び出し元の処理を継続」します。

そのうえで、非同期処理が完了したら、プールしておいた残りの処理を再開します。

async/awaitの挙動

Promiseからの結果はawait演算子の戻り値となるので、そのまま変数にも代入できます。

そして、非同期処理の戻り値(multi(15))もまた、Promiseオブジェクトなので、「.then(response => console.log(`最終結果[${response}]`))」では、非同期関数の結果をthenメソッドで受けて、最終的な処理を行っています。

非同期処理を反復する【Async Iterators】

ES2018ではAsync Iteratorsという機能が導入され、イテレーター/ジェネレーターでもawait演算子を利用できるようになりました。

非同期化したイテレーター/ジェネレーターから値を取得するには、同じく非同期対応のfor await…of命令を利用します。

たとえば以下は、fetchメソッドでarticle1~3を取得し、そのtitleプロパティを列挙する例です。

// 非同期ジェネレーターを定義
async function* fetchIterator () {
  for (let i = 1; i <= 3; i++) {
  // article1~3.jsonを取得
    let result = await fetch(`article${i}.json`);
    yield result.json();
  }
}

// 非同期ジェネレーターからデータを取得&titleプロパティを出力
async function showTitle () {
  for await (let data of fetchIterator()) {
    console.log(data.title);
  }
}
showTitle();
{
  "title":"7つの習慣 人格主義の回復",
  "author":"スティーブン・R.コヴィー",
  "url":"https://www.amazon.co.jp/"
}
{
  "title":"嫌われる勇気",
  "author":"岸見 一郎",
  "url":"https://www.amazon.co.jp/"
}
{
  "title":"ゲッターズ飯田の金持ち風水",
  "author":"ゲッターズ飯田",
  "url":"https://www.amazon.co.jp/"
}
7つの習慣 人格主義の回復
嫌われる勇気
ゲッターズ飯田の金持ち風水

非同期ジェネレーターを定義するには、普通のジェネレーター関数に対してasyncキーワードを付与するだけです。(ソースコード2行目)

これで関数配下でawait演算子を利用できるようになります。

この例であれば、fetchメソッドの戻り値(Promise<Response>オブジェクト)をawait演算子で受け取り、そのjsonメソッドで取得したデータを返しています。(ソースコード5~6行目)

非同期ジェネレーターから値を取り出しているのは、ソースコードの12~14行目です。

await演算子が付いた他は、構文そのものは普通のfor...of命令です。

await演算子を利用しているので、これをくくる関数にはasyncキーワードを付与しなければならない点に注意が必要です。(ソースコード11行目)


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です