Promise
クラスには6つの静的メソッドがあります。ここではそれらのユースケースを簡単に説明します。
Promise.all
多くのPromiseを並行して実行し、すべてが準備完了するまで待機したいとします。
例えば、複数のURLを並行してダウンロードし、すべて完了したらコンテンツを処理するなどです。
これがPromise.all
の目的です。
構文は以下の通りです。
let promise = Promise.all(iterable);
Promise.all
は、イテラブル(通常はPromiseの配列)を受け取り、新しいPromiseを返します。
新しいPromiseは、リストされたすべてのPromiseが解決されると解決され、それらの結果の配列がその結果となります。
例えば、以下のPromise.all
は3秒後に確定し、その結果は配列[1, 2, 3]
になります。
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member
結果の配列メンバーの順序は、元のPromiseと同じであることに注意してください。最初のPromiseの解決に最も時間がかかっても、結果の配列では最初にきます。
一般的な方法は、ジョブデータの配列をPromiseの配列にマッピングし、それをPromise.all
でラップすることです。
例えば、URLの配列がある場合、次のようにすべてをフェッチできます。
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://api.github.com/users/jeresig'
];
// map every url to the promise of the fetch
let requests = urls.map(url => fetch(url));
// Promise.all waits until all jobs are resolved
Promise.all(requests)
.then(responses => responses.forEach(
response => alert(`${response.url}: ${response.status}`)
));
名前でGitHubユーザーの配列のユーザー情報をフェッチするより大きな例(IDで商品の配列をフェッチすることもできます。ロジックは同じです)
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// all responses are resolved successfully
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // shows 200 for every url
}
return responses;
})
// map array of responses into an array of response.json() to read their content
.then(responses => Promise.all(responses.map(r => r.json())))
// all JSON answers are parsed: "users" is the array of them
.then(users => users.forEach(user => alert(user.name)));
いずれかのPromiseが拒否された場合、Promise.all
によって返されるPromiseは、そのエラーですぐに拒否されます。
例えば
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
ここでは、2番目のPromiseが2秒で拒否されます。これにより、Promise.all
がすぐに拒否されるため、.catch
が実行されます。拒否エラーがPromise.all
全体の結果になります。
1つのPromiseが拒否されると、Promise.all
はすぐさま拒否し、リスト内の他のPromiseを完全に無視します。それらの結果は無視されます。
たとえば、上記の例のように複数のfetch
呼び出しがあり、1つが失敗した場合、他の呼び出しは引き続き実行されますが、Promise.all
はそれらを監視しなくなります。それらは多分確定しますが、結果は無視されます。
Promise.all
はそれらをキャンセルするためには何もしません。Promiseには「キャンセル」という概念がないからです。別の章で、それを支援できるAbortController
について説明しますが、それはPromise APIの一部ではありません。
Promise.all(iterable)
は、iterable
にPromiseではない「通常の」値を許可します通常、Promise.all(...)
はPromiseのイテラブル(ほとんどの場合配列)を受け入れます。しかし、それらのオブジェクトのいずれかがPromiseでない場合、結果の配列に「そのまま」渡されます。
例えば、ここで結果は[1, 2, 3]
になります。
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(alert); // 1, 2, 3
したがって、都合の良い場所に、準備完了した値をPromise.all
に渡すことができます。
Promise.allSettled
いずれかのPromiseが拒否されると、Promise.all
は全体として拒否します。これは、すべての結果が成功する必要がある「オールオアナッシング」のケースに適しています。
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render method needs results of all fetches
Promise.allSettled
は、結果に関係なく、すべてのPromiseが確定するのを待機します。結果の配列には以下が含まれます。
- 成功したレスポンスの場合は
{status:"fulfilled", value:result}
、 - エラーの場合は
{status:"rejected", reason:error}
。
例えば、複数のユーザーの情報を取得したいとします。1つのリクエストが失敗した場合でも、他のリクエストには関心があります。
Promise.allSettled
を使用してみましょう
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
上記の(*)
行のresults
は、以下のようになります。
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
したがって、Promiseごとにそのステータスとvalue/error
を取得します。
polyfill
ブラウザがPromise.allSettled
をサポートしていない場合は、polyfillが簡単です
if (!Promise.allSettled) {
const rejectHandler = reason => ({ status: 'rejected', reason });
const resolveHandler = value => ({ status: 'fulfilled', value });
Promise.allSettled = function (promises) {
const convertedPromises = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
return Promise.all(convertedPromises);
};
}
このコードでは、promises.map
は入力値を受け取り、p => Promise.resolve(p)
でそれらをPromiseに変換し(念のためPromiseではないものが渡された場合)、すべてに.then
ハンドラーを追加します。
そのハンドラーは、成功した結果のvalue
を{status:'fulfilled', value}
に変換し、エラーのreason
を{status:'rejected', reason}
に変換します。これはまさにPromise.allSettled
の形式です。
これで、Promise.allSettled
を使用して、一部が拒否された場合でも、指定されたすべてのPromiseの結果を取得できます。
Promise.race
Promise.all
に似ていますが、最初に確定したPromiseのみを待ち、その結果(またはエラー)を取得します。
構文は以下の通りです。
let promise = Promise.race(iterable);
例えば、ここでの結果は1
になります。
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
ここでの最初のPromiseが最速だったため、それが結果になりました。最初に確定したPromiseが「レースに勝つ」と、それ以降の結果/エラーはすべて無視されます。
Promise.any
Promise.race
に似ていますが、最初に解決されたPromiseのみを待機し、その結果を取得します。指定されたすべてのPromiseが拒否された場合、返されるPromiseはAggregateError
で拒否されます。これは、すべてのPromiseエラーをerrors
プロパティに保存する特別なエラーオブジェクトです。
構文は以下の通りです。
let promise = Promise.any(iterable);
例えば、ここでの結果は1
になります。
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
ここでの最初のPromiseが最速でしたが拒否されたため、2番目のPromiseが結果になりました。最初に解決されたPromiseが「レースに勝つ」と、それ以降の結果はすべて無視されます。
すべてのPromiseが失敗した場合の例を次に示します。
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000))
]).catch(error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error!
});
ご覧のとおり、失敗したPromiseのエラーオブジェクトは、AggregateError
オブジェクトのerrors
プロパティで利用できます。
Promise.resolve/reject
Promise.resolve
およびPromise.reject
メソッドは、async/await
構文(後ほど説明します)によってある程度不要になるため、現代のコードではめったに必要ありません。
ここでは、完全を期すため、および何らかの理由でasync/await
を使用できない人のために説明します。
Promise.resolve
Promise.resolve(value)
は、結果value
を使用して解決済みのPromiseを作成します。
以下と同じです
let promise = new Promise(resolve => resolve(value));
このメソッドは、関数がPromiseを返すことが期待される場合の互換性のために使用されます。
例えば、次のloadCached
関数はURLをフェッチし、その内容を記憶(キャッシュ)します。同じURLでの将来の呼び出しの場合、キャッシュから以前の内容をすぐに取得しますが、Promise.resolve
を使用してそのPromiseを作成し、返される値が常にPromiseになるようにします。
let cache = new Map();
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)
}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
関数がPromiseを返すことが保証されているため、loadCached(url).then(…)
と記述できます。loadCached
の後に常に.then
を使用できます。これが(*)
行のPromise.resolve
の目的です。
Promise.reject
Promise.reject(error)
は、error
で拒否されたPromiseを作成します。
以下と同じです
let promise = new Promise((resolve, reject) => reject(error));
実際には、このメソッドはほとんど使用されません。
まとめ
Promise
クラスには6つの静的メソッドがあります。
Promise.all(promises)
– すべてのPromiseが解決されるのを待ち、それらの結果の配列を返します。指定されたPromiseのいずれかが拒否された場合、それはPromise.all
のエラーになり、他のすべての結果は無視されます。Promise.allSettled(promises)
(最近追加されたメソッド)– すべてのPromiseが確定するのを待ち、それらの結果を次のオブジェクトの配列として返します。status
:"fulfilled"
または"rejected"
value
(解決した場合) またはreason
(拒否した場合)。
Promise.race(promises)
– 最初に確定したPromiseを待機し、その結果/エラーが結果になります。Promise.any(promises)
(最近追加されたメソッド)– 最初に解決されるPromiseを待機し、その結果が結果になります。指定されたすべてのPromiseが拒否された場合、AggregateError
がPromise.any
のエラーになります。Promise.resolve(value)
– 指定された値で解決済みのPromiseを作成します。Promise.reject(error)
– 指定されたエラーで拒否されたPromiseを作成します。
これらの中で、Promise.all
が実際には最も一般的です。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen...)を使用してください。