JavaScriptの非同期処理において、Promiseは非常に便利な概念です。
しかし複数の非同期タスクを同時に処理する必要がある場合、どのように取り組めばよいのでしょうか?
そこで登場するのが「Promise.all」です。
スクールや学習教材では意外とPromise.allについては深く説明されていないことが多いです。
本記事ではPromise.allの基本的な使い方から、Promiseとの違いについて詳しく解説していきます。
また通常のPromiseとPromise.allの違いについても説明しますので、初学者の方なら非同期処理の理解を深めることができるでしょう。
また動画でも解説しているので必要に応じて活用してください。
Promise.allとは何をしているのか?
Promiseとは非同期処理で使用するものなのでAPIからデータを取得するようなものを作ってみたいと思います。
使用するAPIはjsonplaceholderになります、無料で簡単に使えるAPIです。
https://jsonplaceholder.typicode.com/
const fetchData = async (count) => {
const array = [];
for (let i = 1; i <= count; i++) {
const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${i}`);
const data = await res.json();
await array.push(data);
}
console.log(array);
};
fetchData(5);
こちらのコードは引数に数字を入れた数だけデータを取得して定数arrayに順番に格納していくものです。
単純にデータを取得するだけではなく、取得したデータを別の配列に入れるという手間があります。
定数arrayはコンソールに出力するようにしていて上手くいくと以下のようになります。
定数arrayは最初は空の配列ですが、fetchDataを実行するときに5を引数に入れたので5個のデータを取得して格納できています。
例えばですが引数に100のような大きい数字を入れてみましょう。
もちろん同じように配列に格納してコンソールで出力することができます。
しかし先ほどと違ってコンソールに出力されるまでの時間が全然違います。
実行環境によりますが私のパソコンだと100は1分近くかかりました。
動作自体は問題ないですが、いざサービスの機能として使うには微妙かと思います。
そういった場合にPromise.allを使った書き方に変えると解決できたりします。
同じ動作を以下のように書き変えます。
const fetchData = async (count) => {
const array = [];
// ここから先を変更
for (let i = 1; i <= count; i++) {
array.push(
fetch(`https://jsonplaceholder.typicode.com/todos/${i}`).then((res) =>
res.json()
)
);
}
const data = await Promise.all(array);
console.log(data);
};
fetchData(100);
大きく変わったように見えますが、Promise.allの引数に定数arrayを指定するのを追加しただけで他の部分は書き方を変えているだけです。
同じような結果を得ることができていますし、引数に100を入れても1分近く待つことが無くなりました。
もともとデータを1件取得→取得したデータを定数arrayに格納という作業を繰り返すわけですが、100件といった大きい数を実行すると時間が掛かるわけです。
専門的なことを言うと2つのPromiseを実行することを100回繰り返すと待機時間が増えるからです。
Promise.allはそのような複数のPromiseを並列(ほぼ同時に)で実行してくれます。
そのためプログラムの内容にもよりますが同じ動作でもPromise.allで実行した方が早く完了することがあります。
初学者の方がスクールや学習教材で非同期処理を学ぶ際には1つのPromiseを処理するものしか学びませんので、 Promise.allは言葉だけ紹介されて使い方を知らない方が多いです。
実務ではたまに複数のPromiseを処理しないといけない場面があるので解決策の一つとして覚えておきましょう。
またPromiseについて理解し直したい方は以下の動画をご覧ください。
Promise.allを使った実例(ファイルサイズの取得)
それではPromise.allを使う実例を紹介したいと思います。
例えば画像を添付する機能があるとして、添付された画像のサイズを確認して何か条件分岐に使用したい場合です。
fetchを使って画像ファイルの情報を取得します、特徴的な場所はメソッドをHEADにしている点です。
ファイルサイズはヘッダー情報に含まれているのでメソッドをPOSTではなくHEADにすると取得できるためです。
// 画像のパス、URLなど
let smUrl = './images/sm-img.jpg';
let midUrl = './images/mid-img.jpg';
let lgUrl = './images/lg-img.jpg';
let options = {
// ヘッダーのみを取得するメソッド
method: 'HEAD',
};
fetch(midUrl, options)
.then((res) => {
console.log(res);
// 通信エラーなどcatchへ渡す
if (!res.ok) throw res.statusText;
//ヘッダーオブジェクトからサイズを取得
console.log(res.headers.get('content-length'));
return res.blob();
})
.then((blob) => {
//そのままURLに変換してページに挿入したりする
console.log(blob);
})
.catch((err) => {
console.log(err.message);
});
今回は3つの画像があるのですがmidUrlという画像だけを実行しました。
そうするとレスポンスオブジェクトが返ってきて画像を取得しますので、あとはblobメソッドを実行してURLに変換したりするわけですね。
サーバーに送信する部分については割愛します。
しかし最近では複数画像のアップロードが当たり前なので、画像の読み込みを1件ずつ順番にやると時間がかかるアプリになってしまいますのでPromise.allを使います。
let smUrl = './images/sm-img.jpg';
let midUrl = './images/mid-img.jpg';
let lgUrl = './images/lg-img.jpg';
let options = {
method: 'HEAD',
};
// ここから変更
let files = {};
Promise.all([
fetch(smUrl, options),
fetch(midUrl, options),
fetch(lgUrl, options),
])
.then(([s, m, l]) => {
console.log(s);
console.log(m);
console.log(l);
})
.catch((err) => {
console.warn(err.message);
});
レスポンスオブジェクトが3つ返ってきているのが分かります。
コードのポイントしては、Promise.allの引数に配列にして3つの画像のパスを入れました。
引数を配列にするとレスポンスオブジェクトも配列になって返ってきますので、thenの引数も配列にしています。
ファイルサイズを取得するにはheadersプロパティからcontent-lengthというものを取り出します。
let smUrl = './images/sm-img.jpg';
let midUrl = './images/mid-img.jpg';
let lgUrl = './images/lg-img.jpg';
let options = {
method: 'HEAD',
};
let files = {};
Promise.all([
fetch(smUrl, options),
fetch(midUrl, options),
fetch(lgUrl, options),
])
.then(([s, m, l]) => {
// ここから変更
console.log('sm:', s.headers.get('content-length'));
console.log('mid:', m.headers.get('content-length'));
console.log('lg:', l.headers.get('content-length'));
})
.catch((err) => {
console.warn(err.message);
});
数字が表示されており、こちらがそれぞれのファイルサイズになります。
あとは条件分岐など他の処理に使用することを考えて、パス情報とサイズの組み合わせなどでオブジェクトに格納しておく感じです。
let smUrl = './images/sm-img.jpg';
let midUrl = './images/mid-img.jpg';
let lgUrl = './images/lg-img.jpg';
let options = {
method: 'HEAD',
};
let files = {};
Promise.all([
fetch(smUrl, options),
fetch(midUrl, options),
fetch(lgUrl, options),
])
.then(([s, m, l]) => {
// ここから変更
files[new URL(s.url).pathname] = s.headers.get('content-length');
files[new URL(m.url).pathname] = m.headers.get('content-length');
files[new URL(l.url).pathname] = l.headers.get('content-length');
console.log({ files });
})
.catch((err) => {
console.warn(err.message);
});
後はサイズによっては添付できないようにするなどを追加で作ることになります。
fetchメソッドのような非同期処理を複数実行するときにPromise.allを使ってみましょう。