Javascript Promise API

2023-02-17 10:53 更新

在 ?Promise? 類中,有 6 種靜態(tài)方法。我們?cè)谶@里簡(jiǎn)單介紹下它們的使用場(chǎng)景。

Promise.all

假設(shè)我們希望并行執(zhí)行多個(gè) promise,并等待所有 promise 都準(zhǔn)備就緒。

例如,并行下載幾個(gè) URL,并等到所有內(nèi)容都下載完畢后再對(duì)它們進(jìn)行處理。

這就是 Promise.all 的用途。

語法:

let promise = Promise.all(iterable);

Promise.all 接受一個(gè)可迭代對(duì)象(通常是一個(gè)數(shù)組項(xiàng)為 promise 的數(shù)組),并返回一個(gè)新的 promise。

當(dāng)所有給定的 promise 都 resolve 時(shí),新的 promise 才會(huì) resolve,并且其結(jié)果數(shù)組將成為新 promise 的結(jié)果。

例如,下面的 Promise.all 在 3 秒之后 settled,然后它的結(jié)果就是一個(gè) [1, 2, 3] 數(shù)組:

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 當(dāng)上面這些 promise 準(zhǔn)備好時(shí):每個(gè) promise 都貢獻(xiàn)了數(shù)組中的一個(gè)元素

請(qǐng)注意,結(jié)果數(shù)組中元素的順序與其在源 promise 中的順序相同。即使第一個(gè) promise 花費(fèi)了最長(zhǎng)的時(shí)間才 resolve,但它仍是結(jié)果數(shù)組中的第一個(gè)。

一個(gè)常見的技巧是,將一個(gè)任務(wù)數(shù)據(jù)數(shù)組映射(map)到一個(gè) promise 數(shù)組,然后將其包裝到 Promise.all。

例如,如果我們有一個(gè)存儲(chǔ) URL 的數(shù)組,我們可以像這樣 fetch 它們:

let urls = [
  'https://api.github.com/users/iliakan',
  'https://api.github.com/users/remy',
  'https://api.github.com/users/jeresig'
];

// 將每個(gè) url 映射(map)到 fetch 的 promise 中
let requests = urls.map(url => fetch(url));

// Promise.all 等待所有任務(wù)都 resolved
Promise.all(requests)
  .then(responses => responses.forEach(
    response => alert(`${response.url}: ${response.status}`)
  ));

一個(gè)更真實(shí)的示例,通過 GitHub 用戶名來獲取一個(gè) GitHub 用戶數(shù)組中用戶的信息(我們也可以通過商品 id 來獲取商品數(shù)組中的商品信息,邏輯都是一樣的):

let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
  .then(responses => {
    // 所有響應(yīng)都被成功 resolved
    for(let response of responses) {
      alert(`${response.url}: ${response.status}`); // 對(duì)應(yīng)每個(gè) url 都顯示 200
    }

    return responses;
  })
  // 將響應(yīng)數(shù)組映射(map)到 response.json() 數(shù)組中以讀取它們的內(nèi)容
  .then(responses => Promise.all(responses.map(r => r.json())))
  // 所有 JSON 結(jié)果都被解析:"users" 是它們的數(shù)組
  .then(users => users.forEach(user => alert(user.name)));

如果任意一個(gè) promise 被 reject,由 Promise.all 返回的 promise 就會(huì)立即 reject,并且?guī)в械木褪沁@個(gè) error。

例如:

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!

這里的第二個(gè) promise 在兩秒后 reject。這立即導(dǎo)致了 Promise.all 的 reject,因此 .catch 執(zhí)行了:被 reject 的 error 成為了整個(gè) Promise.all 的結(jié)果。

如果出現(xiàn) error,其他 promise 將被忽略

如果其中一個(gè) promise 被 reject,Promise.all 就會(huì)立即被 reject,完全忽略列表中其他的 promise。它們的結(jié)果也被忽略。

例如,像上面那個(gè)例子,如果有多個(gè)同時(shí)進(jìn)行的 fetch 調(diào)用,其中一個(gè)失敗,其他的 fetch 操作仍然會(huì)繼續(xù)執(zhí)行,但是 Promise.all 將不會(huì)再關(guān)心(watch)它們。它們可能會(huì) settle,但是它們的結(jié)果將被忽略。

Promise.all 沒有采取任何措施來取消它們,因?yàn)?promise 中沒有“取消”的概念。在 另一個(gè)章節(jié) 中,我們將介紹可以幫助我們解決這個(gè)問題(譯注:指的是“取消” promise)的 AbortController,但它不是 Promise API 的一部分。

?Promise.all(iterable)? 允許在 ?iterable? 中使用非 promise 的“常規(guī)”值

通常,Promise.all(...) 接受含有 promise 項(xiàng)的可迭代對(duì)象(大多數(shù)情況下是數(shù)組)作為參數(shù)。但是,如果這些對(duì)象中的任何一個(gè)不是 promise,那么它將被“按原樣”傳遞給結(jié)果數(shù)組。

例如,這里的結(jié)果是 [1, 2, 3]

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000)
  }),
  2,
  3
]).then(alert); // 1, 2, 3

所以我們可以在方便的地方將準(zhǔn)備好的值傳遞給 Promise.all。

Promise.allSettled

最近新增的特性

這是一個(gè)最近添加到 JavaScript 的特性。 舊式瀏覽器可能需要 polyfills.

如果任意的 promise reject,則 Promise.all 整個(gè)將會(huì) reject。當(dāng)我們需要 所有 結(jié)果都成功時(shí),它對(duì)這種“全有或全無”的情況很有用:

Promise.all([
  fetch('/template.html'),
  fetch('/style.css'),
  fetch('/data.json')
]).then(render); // render 方法需要所有 fetch 的數(shù)據(jù)

Promise.allSettled 等待所有的 promise 都被 settle,無論結(jié)果如何。結(jié)果數(shù)組具有:

  • ?{status:"fulfilled", value:result}? 對(duì)于成功的響應(yīng),
  • ?{status:"rejected", reason:error}? 對(duì)于 error。

例如,我們想要獲?。╢etch)多個(gè)用戶的信息。即使其中一個(gè)請(qǐng)求失敗,我們?nèi)匀粚?duì)其他的感興趣。

讓我們使用 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 將會(huì)是:

[
  {status: 'fulfilled', value: ...response...},
  {status: 'fulfilled', value: ...response...},
  {status: 'rejected', reason: ...error object...}
]

所以,對(duì)于每個(gè) promise,我們都得到了其狀態(tài)(status)和 value/reason

Polyfill

如果瀏覽器不支持 Promise.allSettled,很容易進(jìn)行 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) 將輸入值轉(zhuǎn)換為 promise(以防傳遞了非 promise 值),然后向每一個(gè) promise 都添加 .then 處理程序。

這個(gè)處理程序?qū)⒊晒Φ慕Y(jié)果 value 轉(zhuǎn)換為 {status:'fulfilled', value},將 error reason 轉(zhuǎn)換為 {status:'rejected', reason}。這正是 Promise.allSettled 的格式。

然后我們就可以使用 Promise.allSettled 來獲取 所有 給定的 promise 的結(jié)果,即使其中一些被 reject。

Promise.race

與 Promise.all 類似,但只等待第一個(gè) settled 的 promise 并獲取其結(jié)果(或 error)。

語法:

let promise = Promise.race(iterable);

例如,這里的結(jié)果將是 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

這里第一個(gè) promise 最快,所以它變成了結(jié)果。第一個(gè) settled 的 promise “贏得了比賽”之后,所有進(jìn)一步的 result/error 都會(huì)被忽略。

Promise.any

與 Promise.race 類似,區(qū)別在于 Promise.any 只等待第一個(gè) fulfilled 的 promise,并將這個(gè) fulfilled 的 promise 返回。如果給出的 promise 都 rejected,那么返回的 promise 會(huì)帶有 AggregateError —— 一個(gè)特殊的 error 對(duì)象,在其 errors 屬性中存儲(chǔ)著所有 promise error。

語法如下:

let promise = Promise.any(iterable);

例如,這里的結(jié)果將是 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

這里的第一個(gè) promise 是最快的,但 rejected 了,所以第二個(gè) promise 則成為了結(jié)果。在第一個(gè) fulfilled 的 promise “贏得比賽”后,所有進(jìn)一步的結(jié)果都將被忽略。

這是一個(gè)所有 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!
});

正如你所看到的,我們?cè)?nbsp;AggregateError 錯(cuò)誤類型的 error 實(shí)例的 errors 屬性中可以訪問到失敗的 promise 的 error 對(duì)象。

Promise.resolve/reject

在現(xiàn)代的代碼中,很少需要使用 Promise.resolve 和 Promise.reject 方法,因?yàn)?nbsp;async/await 語法(我們會(huì)在 稍后 講到)使它們變得有些過時(shí)了。

完整起見,以及考慮到那些出于某些原因而無法使用 async/await 的人,我們?cè)谶@里對(duì)它們進(jìn)行介紹。

Promise.resolve

Promise.resolve(value) 用結(jié)果 value 創(chuàng)建一個(gè) resolved 的 promise。

如同:

let promise = new Promise(resolve => resolve(value));

當(dāng)一個(gè)函數(shù)被期望返回一個(gè) promise 時(shí),這個(gè)方法用于兼容性。(譯注:這里的兼容性是指,我們直接從緩存中獲取了當(dāng)前操作的結(jié)果 value,但是期望返回的是一個(gè) promise,所以可以使用 Promise.resolve(value) 將 value “封裝”進(jìn) promise,以滿足期望返回一個(gè) promise 的這個(gè)需求。)

例如,下面的 loadCached 函數(shù)獲?。╢etch)一個(gè) URL 并記住其內(nèi)容。以便將來對(duì)使用相同 URL 的調(diào)用,它能立即從緩存中獲取先前的內(nèi)容,但使用 Promise.resolve 創(chuàng)建了一個(gè)該內(nèi)容的 promise,所以返回的值始終是一個(gè) 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;
    });
}

我們可以使用 loadCached(url).then(…),因?yàn)樵摵瘮?shù)保證了會(huì)返回一個(gè) promise。我們就可以放心地在 loadCached 后面使用 .then。這就是 (*) 行中 Promise.resolve 的目的。

Promise.reject

Promise.reject(error) 用 error 創(chuàng)建一個(gè) rejected 的 promise。

如同:

let promise = new Promise((resolve, reject) => reject(error));

實(shí)際上,這個(gè)方法幾乎從未被使用過。

總結(jié)

?Promise? 類有 6 種靜態(tài)方法:

  1. ?Promise.all(promises)? —— 等待所有 promise 都 resolve 時(shí),返回存放它們結(jié)果的數(shù)組。如果給定的任意一個(gè) promise 為 reject,那么它就會(huì)變成 ?Promise.all? 的 error,所有其他 promise 的結(jié)果都會(huì)被忽略。
  2. ?Promise.allSettled(promises)?(ES2020 新增方法)—— 等待所有 promise 都 settle 時(shí),并以包含以下內(nèi)容的對(duì)象數(shù)組的形式返回它們的結(jié)果:
    • ?status?: ?"fulfilled"? 或 ?"rejected"?
    • ?value?(如果 fulfilled)或 ?reason?(如果 rejected)。
  3. ?Promise.race(promises)? —— 等待第一個(gè) settle 的 promise,并將其 result/error 作為結(jié)果返回。
  4. ?Promise.any(promises)?(ES2021 新增方法)—— 等待第一個(gè) fulfilled 的 promise,并將其結(jié)果作為結(jié)果返回。如果所有 promise 都 rejected,?Promise.any? 則會(huì)拋出 ?AggregateError? 錯(cuò)誤類型的 error 實(shí)例。
  5. ?Promise.resolve(value)? —— 使用給定 value 創(chuàng)建一個(gè) resolved 的 promise。
  6. ?Promise.reject(error)? —— 使用給定 error 創(chuàng)建一個(gè) rejected 的 promise。

以上所有方法,Promise.all 可能是在實(shí)戰(zhàn)中使用最多的。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)