はじめに
JavaScriptにおける非同期処理は、APIを叩いたり、データベースへのアクセスな度を行う際に頻繁に用いられますが、コールバック関数をはじめとした非同期処理の構文は複数ある上に、仕組みそのものがやや難解な部分が多いと思います。今回はそのうち、async/awaitを使った非同期処理において、より迅速な処理を行うためにPromise.allを使うパターンについて記録したいと思います。
async/awaitについて
async/awaitは、ES8(ECMAScript2017)から導入された、非同期処理をより簡潔に記述するための記法です。async/awaitについて、MDNでは、以下のように説明されています。
async
async function キーワードは、式の中で async 関数を定義するために使用できます。
非同期関数は、 async function 文を使用して定義することもできます。
await
await 演算子はプロミス (Promise) を待つために使用します。通常の JavaScript コードで、 async function の内部でのみ使用することができます。によって Promise が返されるのを待機するために使用します。
async/await 地獄!?
ここからが本題になります。async/await構文は、従来のコールバック関数や.then
で処理を繋ぐPromiseよりも簡潔に非同期処理を記述できますが、上記の例で先述した通り、複数のawaitが1つのasync関数に存在する場合、パフォーマンスの低下を引き起こす可能性があります。
下記の例では、単一のasync関数内でユーザーデータを取得を行うawait文と、投稿情報を取得するためのawait文が実行されています。コードを見ると、await getPostItems()
はawait getUserData(id)
の結果に依存した処理ではありません。しかし、この場合、await getUserData(id)
の処理が完了するまで、await getPostItems()
の処理は待つことになってしまいます。また、今後このasync関数内で追加でawait文が3つ、4つ...と実行されることになった場合、実行速度の観点から見たパフォーマンスはさらに低下してしまいます。
// async部分省略
// ユーザーデータを取得
const userData = await getUserData(id);
// 投稿情報を取得
const PostItems = await getPostItems();
return { userData, PostItems };
下のように、即時実行関数で記述してこの問題を解決しようとしても、1つ目のawaitが実行されている間はasync内の処理は停止するので、結局 await getUserData(id)
の実行後に await getPostItems()
が実行されてしまいます。
(async () => {
const userData = await getUserData(id);
const PostItems = await getPostItems();
return { userData, PostItems }
})()
解決策: Promise.all
を使って配列でPromiseを受け取る
記事タイトルや冒頭でもすべに述べていますが、非同期関数内のうちPromiseの解決順序を問わない場合は、実行速度の観点から、Promise.allを使おうということです。以下、MDNのPromise.allについての説明です。
Promise.all() メソッドは入力としてプロミスの集合の反復可能オブジェクトを取り、入力したプロミスの集合の結果の配列に解決される単一の Promise を返します。この返却されたプロミスは、入力したプロミスがすべて解決されるか、入力した反復可能オブジェクトにプロミスが含まれていない場合に解決されます。入力したプロミスのいずれかが拒否されるか、プロミス以外のものがエラーを発生させると直ちに拒否され、最初に拒否されたメッセージまたはエラーをもって拒否されます。
説明にもある通り、配列内の反復可能オブジェクトの処理が並列で行われ、解決された場合に、単一のプロミスとして配列を返します。また、いずれかのプロミスが拒否されるかエラーが発生した場合は、該当のエラーとメッセージを返してくれます。これらを踏まえて、先程の処理をPromise.allで記述したものが下のコードになります。この場合、await getUserData(id)
と await getPostItems()
が同時に実行され、各処理のPromiseを1つの配列として受け取ることができます。
const [ userData, PostItems ] = await Promise.all([ getUserData(id), getPostItems()]);
return { userData, PostItems };
また、この例では非同期処理が2つのみでやや変化を感じにくいですが、より多くのawaitが記述されるようなケースでは、実行速度の改善だけでなく、await特有の冗長さが無くなり、記述もスッキリするかと思います。
最後に
今回は、Promise.allについてご紹介しました。非同期処理は、頻繁に使われることが非常に多いですが、実行完了まで時間がかかる処理が多い故に、システムのパフォーマンスに与える影響は、大きいと思います。今回の例のように、解決順序に依存しない複数の非同期処理がある場合は、Promise.allを使って、実行速度のチューニング等が行えないか都度考える必要がありそうです。
自分が書いたコードも見直してみます。。