0%

Promise(V) - Promise API

本文開始

在 Promise 類別中,有提供六個 static method。

Promise.all

假設,我們希望多個 promise 可以同時執行並且會等到這些 promise 都執行完畢後,才往下執行後續的動作。
舉例,今天我們同時發送下載多個 URL 的 request,並在這些 request 都執行完畢之後,再去處理回傳回來的內容。
那以上這種情境就是 Promise.all 的使用時機。
它的語法如下

1
const promise = Promise.all(iterable);

可以看到在 Promise.all 裡面的參數型別為可遍歷的 (通常會是傳 array 進去),最後,回傳一個新的 Promise 回來。
這一個回傳回來的新的 Promise 會在 Promise.all 裡面的所有 promise 都 resolve 之後,才 resolve,並且這些列表裡面的 promise 的結果們,會合在一起,變成這個 Promise 的結果。

舉個範例

1
2
3
4
5
6
Promise.all([
new Promise(resolve => setTimeout(() => resolve('1'), 1000)),
new Promise(resolve => setTimeout(() => resolve('2'), 2000)),
new Promise(resolve => setTimeout(() => resolve('3'), 3000)),
])
.then(res => console.log(res)); // ['1', '2', '3']

上面這個範例,應該會在三秒後,才在 .thenconsole.log 一次看到所有的內容。

再來一個範例,先來看看它的內容

1
2
3
4
5
6
7
8
9
10
const urls = [
'https://randomuser.me/api/?seed=1',
'https://randomuser.me/api/?seed=2',
'https://randomuser.me/api/?seed=3'
]

const requests = urls.map(url => fetch(url).then(res => res.json()));

Promise.all(requests)
.then(res => res.forEach(item => console.log(item.results[0].name)))

把上面的範例執行的過程拆解一下
.先把所有要送出 request 的 url 放到 urls 陣列裡面
.使用 fetch 送出 request,接著,fetch 會回傳 Promise 型別的回傳內容

Note:
這邊有先用 response.json() 這個方法把回傳的內容轉成 json 型別,
不然,沒有做轉型的話,我們會沒有辦法看到回傳的資料內容。

接著,再用 map 把這個回傳回來的 Promise 型態的回傳結果,存到 requests 陣列裡面。
最後,requests 陣列就會存有三個 Promise ,而它們會存有它們自己的回傳內容。
.最後,用 Promise.all 來解析出所有 requests 裡面的 Promise 內容。
並利用 Promise.all 的 .then 來顯示出所有個回傳內容。

如果 Promise.all 發生錯誤的話

只要在 Promise.all 裡其中一個 Promise 被 reject 則 Promise.all 會立即回傳帶有 error 的 promise

1
2
3
4
5
6
7
Promise.all([
new Promise(resolve => setTimeout(() => resolve('1'), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error('whooop!')), 2000)),
new Promise(resolve => setTimeout(() => resolve('3'), 3000)),
])
.then(res => console.log(res))
.catch(alert) // Whooop!

以上的範例,第二個 Promise 被 reject,這時 Promise.all 回傳的就會是帶有 error 的 Promise,並且觸發上面範例的 .catch

Note: 一但有 Promise 發生 reject,就終止不管其他的 promise 了
一但 Promise.all 接收到有 promise reject,則其他已經被啟動的 Promise 將會被忽略,即若這些 request 都是透過 fetch 來啟動的,但有一個 Promise 發生了 reject,則 Promise.all 就不會管其他正在執行的 fetch request,更別說它們的回傳值,也不會去理它們的回傳值了。

Promise.allSettled

我們現在知道 Promise.all 會直接 reject 當其中一個 promise 發出 reject 的時候,不管其他的 promise 的執行結果,由此可知 Promise.all 很適合用在全有全無的情境中。
Promise.allSettled 的特性就跟 Promise.all 相反,它會等到所有的 promise 狀態都是 settled 了 (即: promise 都呼叫了 resolve 或 reject 了),才會繼續往下執行。
Promise.allSettled 產出的陣列中的元素,其內容會有下面兩種

1
2
{status: "fulfilled", value: result} // for successful response
{status: "rejected", reason:error} // for errors

以下就來舉個 Promise.allSettled 的範例

1
2
3
4
5
6
7
8
9
10
const urls = [
'https://randomuser.me/api/?seed=1',
'https://randomuser.me/api/?seed=2',
'https://randomuser.me/errorUrl'
]

const requests = urls.map(url => fetch(url).then(res => res.json()))

Promise.allSettled(requests)
.then(results => console.log(results))

以上的範例,在 console.log 印出來的結果如下

由以上的範例可以知道,我們可以透過 Promise.allSettled 最後的陣列結果,取得每一個 promise 的 status 和 value/error。

Promise.allSettled - polyfill version

在這一篇教學文章中,它有直接寫出用 Promise.all 建構出 Promise.allSettled 的功能

1
2
3
4
5
6
7
8
9
10
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);
};
}

上面的範例來拆解一下

.先定義了 resolveHandler 和 rejectHandler ,分別會放在每一個 promise 的 .then 中的第一個參數和第二個參數。
.接著,使用 Promise.resolve 除了用來預防將傳入的內容不是 Promise 型別的內容轉成 Promise 型別之外,用 Promise.resolve 就等同於直接呼叫 resolve ,不管其結果是 reject 還是 resolve 產生的。
會需要用 Promise.resolv 的原因是因為 Promise.all 只要一個 promise 不是 resolve 就會直接回傳 reject 的結果,但這不是 Promise.allSettled 要的效果,Promise.allSettled 只要是 promise 的狀態是 settled 就好,不管你是 reject 或 resolve。
.promises.map 則把 resolve 完成後的 promise 都蒐集起來,並存到 convertedPromises 中。
.最後再由 Promise.all 把這些已經都 resolve 後的 promise 回傳回去。

因為,筆者是在 Chrome 瀏覽器上做測試,所以,不太能測試上面範例的內容,所以,稍微改寫一下上面範例的內容,來測試自訂義用 Promise.all 寫出來的 Promise.allSettled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function customizedPromiseAllSettled(promises) {
const resolveHandler = value => ({status:'fulfilled', value});
const rejectHandler = reason => ({status:'rejected', reason});

const convertPromise = promises.map(p => Promise.resolve(p).then(resolveHandler, rejectHandler));
return Promise.all(convertPromise);
}

const urls = [
'https://randomuser.me/api/?seed=1',
'https://randomuser.me/api/?seed=2',
'https://randomuser.me/errorUrl'
]

const requests = urls.map(url => fetch(url).then(res => res.json()));
customizedPromiseAllSettled(requests)
.then(results => results.forEach(res => console.log(res)))

以上這個改寫的範例,應該可以在 console.log 中看到以下內容

Promise.race

這個方法只會取第一個狀態纖維 settled(呼叫 resolve 或 reject 後的狀態) 的 promise 的結果。

其語法如下

1
const promise = Promise.race(iterable);

語法跟 Promise.all 差不多,傳入的參數也是要為一個可被遍歷的內容。

直接上個例子

1
2
3
4
5
6
7
const promise = Promise.race([
new Promise(resolve => setTimeout(() => resolve('1'), 1000)),
new Promise(reject=> setTimeout(() => reject(new Error('This is an error')), 3000)),
new Promise(resolve => setTimeout(() => resolve('2'), 2000))
])

promise.then(res => console.log(res)) // 1

上面的範例第一個 settled 的 promise 就是 延遲一秒的那一個 promise。

Promise.any

這個方法會等第一個 status 改為 fulfilled 的 promise,並將它當為最終結果,並忽略其他 promise 的結果。
如果,在 Promise.any 裡的所有 promise 都為 rejected 的話,則最終會回傳一個呼叫 reject 的 Promise,並且夾帶著一個特別的錯誤物件 AggregateError ,而這個物件會夾帶所有 promise 所回傳的 error 內容在它的 errors 屬性中。

其語法為

1
const promise = Promise.any(iterable)

先來個範例

1
2
3
4
5
6
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error('This is an error')), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve('3'), 3000)),
new Promise((resolve, reject) => setTimeout(() => resolve('2'), 2000))
])
.then(res => console.log(res)) // 2

上面的範例,故意讓最先完成的延遲一秒個部分, 呼叫 reject,所以,Promise.any 最終的結果會是在延遲兩秒後,回傳 “2” 的結果。

以下來個 Promise 全部都呼叫 reject 的範例

1
2
3
4
5
6
7
8
9
10
11
12
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("First Error!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Second Error!")), 2000)),
])
.then(
res => console.log(res),
error => {
console.log(error.constructor.name); // AggregateError
console.log(error.errors[0]); // Error: Ouch!
console.log(error.errors[1]); // Error: Error!
}
)

以上範例,可以在 console.log 看到以下結果

Promise.resolve/reject

Promise.resolve

Promise.resolve(value) 這個方法會回傳一個呼叫 resolve 的 Promise,並夾帶著結果 value

其效力等同於

1
const promise = new Promise(resolve => resolve(value));

以下為做出一個儲存或載入暫存資料的 function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const cache = new Map();

function loadCache(url) {
if(cache.get(url)) {
return Promise.resolve(cache.get(url));
}

return fetch(url)
.then(res => res.json())
.then(res => {
cache.set(url, res.results[0].name);
return `${res.results[0].name['title']} ${res.results[0].name['first']} ${res.results[0].name['last']}`;
});
}

loadCache('https://randomuser.me/api/?seed=1').then(res => console.log(res));
loadCache('https://randomuser.me/api/?seed=1').then(res => console.log(res));

Promise.reject

Promise.reject(error) 這個方法會回傳一個呼叫 reject 後的 Promise,並夾帶著 error。
其效力等同於

1
const promise = new Promise(reject => reject(error));

Reference

  1. https://javascript.info/promise-api