初学者应该看的JavaScript Promise 完整指南

开发
这篇文章算是 JavaScript Promises 比较全面的教程,该文介绍了必要的方法,例如 then,catch和finally。此外,还包括处理更复杂的情况,例如与Promise.all并行执行Promise,通过Promise.race 来处理请求超时的情况,Promise 链以及一些最佳实践和常见的陷阱。

1.JavaScript Promises
Promise 是一个允许我们处理异步操作的对象,它是 es5 早期回调的替代方法。

[[342689]]

与回调相比,Promise 具有许多优点,例如:

让异步代码更易于阅读。
提供组合错误处理。* 更好的流程控制,可以让异步并行或串行执行。
回调更容易形成深度嵌套的结构(也称为回调地狱)。如下所示:

  1. a(() => { 
  2.   b(() => { 
  3.     c(() => { 
  4.       d(() => { 
  5.         // and so on ... 
  6.       });    });  });}); 

如果将这些函数转换为 Promise,则可以将它们链接起来以生成更可维护的代码。像这样:

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(c) 
  5.   .then(d) 
  6.   .catch(console.error); 

在上面的示例中,Promise 对象公开了.then和.catch方法,我们稍后将探讨这些方法。

1.1 如何将现有的回调 API 转换为 Promise?
我们可以使用 Promise 构造函数将回调转换为 Promise。

Promise 构造函数接受一个回调,带有两个参数resolve和reject。

Resolve:是在异步操作完成时应调用的回调。
Reject:是发生错误时要调用的回调函数。
构造函数立即返回一个对象,即 Promise 实例。当在 promise 实例中使用.then方法时,可以在Promise “完成” 时得到通知。让我们来看一个例子。

Promise 仅仅只是回调?
并不是。承诺不仅仅是回调,但它们确实对.then和.catch方法使用了异步回调。Promise 是回调之上的抽象,我们可以链接多个异步操作并更优雅地处理错误。来看看它的实际效果。

Promise 反面模式(Promises 地狱)

  1. a(() => { 
  2.   b(() => { 
  3.     c(() => { 
  4.       d(() => { 
  5.         // and so on ... 
  6.       });    });  });}); 

不要将上面的回调转成下面的 Promise 形式:

  1. a().then(() => { 
  2.   return b().then(() => { 
  3.     return c().then(() => { 
  4.       return d().then(() =>{ 
  5.         // ⚠️ Please never ever do to this! ⚠️ 
  6.       });    });  });}); 

上面的转成,也形成了 Promise 地狱,千万不要这么转。相反,下面这样做会好点:

  1. a() 
  2.   .then(b) 
  3.   .then(c) 
  4.   .then(d) 

超时
你认为以下程序的输出的是什么?

  1. const promise = new Promise((resolve, reject) => { 
  2.   setTimeout(() => { 
  3.     resolve('time is up ⏰'); 
  4.   }, 1e3); 
  5.   setTimeout(() => { 
  6.     reject('Oops '); 
  7.   }, 2e3); 
  8. });promise  .then(console.log) 
  9.   .catch(console.error); 

是输出:

  1. time is up ⏰ 
  2. Oops!  

还是输出:

  1. time is up ⏰ 

是后者,因为当一个Promise resolved 后,它就不能再被rejected。

一旦你调用一种方法(resolve 或reject),另一种方法就会失效,因为 promise 处于稳定状态。让我们探索一个 promise 的所有不同状态。

1.2 Promise 状态
Promise 可以分为四个状态:

⏳ Pending:初始状态,异步操作仍在进行中。
✅ Fulfilled:操作成功,它调用.then回调,例如.then(onSuccess)。
⛔️ Rejected: 操作失败,它调用.catch或.then的第二个参数(如果有)。例如.catch(onError)或.then(..., onError)。
Settled:这是 promise 的最终状态。promise 已经死亡了,没有别的办法可以解决或拒绝了。.finally方法被调用

1.3 Promise 实例方法
Promise API 公开了三个主要方法:then,catch和finally。我们逐一配合事例探讨一下。

Promise then
then方法可以让异步操作成功或失败时得到通知。它包含两个参数,一个用于成功执行,另一个则在发生错误时使用。

  1. promise.then(onSuccess, onError); 

你还可以使用catch来处理错误:

  1. promise.then(onSuccess).catch(onError); 

Promise 链
then 返回一个新的 Promise ,这样就可以将多个Promise 链接在一起。就像下面的例子一样:

  1. Promise.resolve() 
  2.   .then(() => console.log('then#1')) 
  3.   .then(() => console.log('then#2')) 
  4.   .then(() => console.log('then#3')); 

Promise.resolve立即将Promise 视为成功。因此,以下所有内容都将被调用。输出将是

  1. then#1 
  2. then#2 
  3. then#3 

Promise catch
Promise .catch方法将函数作为参数处理错误。如果没有出错,则永远不会调用catch方法。

假设我们有以下承诺:1秒后解析或拒绝并打印出它们的字母。

  1. const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3)); 
  2. const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 1e3)); 
  3. const c = () => new Promise((resolve, reject) => setTimeout(() => { console.log('c'), reject('Oops!') }, 1e3)); 
  4. const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 1e3)); 

请注意,c使用reject('Oops!')模拟了拒绝。

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(c) 
  5.   .then(d) 
  6.   .catch(console.error) 

输出如下:

在这种情况下,可以看到a,b和c上的错误消息。

我们可以使用then函数的第二个参数来处理错误。但是,请注意,catch将不再执行。

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(c) 
  5.   .then(d, () => console.log('c errored out but no big deal')) 
  6.   .catch(console.error) 

由于我们正在处理 .then(..., onError)部分的错误,因此未调用catch。d不会被调用。如果要忽略错误并继续执行Promise链,可以在c上添加一个catch。像这样:

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(() => c().catch(() => console.log('error ignored'))) 
  5.   .then(d) 
  6.   .catch(console.error) 

当然,这种过早的捕获错误是不太好的,因为容易在调试过程中忽略一些潜在的问题。

Promise finally
finally方法只在 Promise 状态是 settled 时才会调用。

如果你希望一段代码即使出现错误始终都需要执行,那么可以在.catch之后使用.then。

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(c) 
  5.   .then(d) 
  6.   .catch(console.error) 
  7.   .then(() => console.log('always called')); 

或者可以使用.finally关键字:

  1. Promise.resolve() 
  2.   .then(a) 
  3.   .then(b) 
  4.   .then(c) 
  5.   .then(d) 
  6.   .catch(console.error) 
  7.   .finally(() => console.log('always called')); 

1.4 Promise 类方法
我们可以直接使用 Promise 对象中四种静态方法。

 

  1. Promise.all 
  2. Promise.reject 
  3. Promise.resolve 
  4. Promise.race 

 

Promise.resolve 和 Promise.reject
这两个是帮助函数,可以让 Promise 立即解决或拒绝。可以传递一个参数,作为下次 .then 的接收:

  1. Promise.resolve('Yay!!!'
  2.   .then(console.log) 
  3.   .catch(console.error) 

上面会输出 Yay!!!

  1. Promise.reject('Oops '
  2.   .then(console.log) 
  3.   .catch(console.error) 

使用 Promise.all 并行执行多个 Promise
通常,Promise 是一个接一个地依次执行的,但是你也可以并行使用它们。

假设是从两个不同的api中轮询数据。如果它们不相关,我们可以使用Promise.all()同时触发这两个请求。

在此示例中,主要功能是将美元转换为欧元,我们有两个独立的 API 调用。一种用于BTC/USD,另一种用于获得EUR/USD。如你所料,两个 API 调用都可以并行调用。但是,我们需要一种方法来知道何时同时完成最终价格的计算。我们可以使用Promise.all,它通常在启动多个异步任务并发运行并为其结果创建承诺之后使用,以便人们可以等待所有任务完成。

  1. const axios = require('axios'); 
  2. const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets'); 
  3. const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD'); 
  4. const currency = 'EUR'
  5. // Get the price of bitcoins on 
  6. Promise.all([bitcoinPromise, dollarPromise])  .then(([bitcoinMarkets, dollarExchanges]) => { 
  7.     const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD'
  8.     const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc)    const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price;    const rate = dollarExchanges.data.rates[currency];    return rate * coinbaseBtcInUsd; 
  9.   })  .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`)) 
  10.   .catch(console.log) 

如你所见,Promise.all接受了一系列的 Promises。当两个请求的请求都完成后,我们就可以计算价格了。

我们再举一个例子:

  1. const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000)); 
  2. const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000)); 
  3. const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000)); 
  4. const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000)); 
  5. console.time('promise.all'); 
  6. Promise.all([a(), b(), c(), d()])  .then(results => console.log(`Done! ${results}`)) 
  7.   .catch(console.error) 
  8.   .finally(() => console.timeEnd('promise.all')); 

解决这些 Promise 要花多长时间?5秒?1秒?还是2秒?

这个留给你们自己验证咯。

Promise race
Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

  1. const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000)); 
  2. const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000)); 
  3. const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000)); 
  4. const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000)); 
  5. console.time('promise.race'); 
  6. Promise.race([a(), b(), c(), d()])  .then(results => console.log(`Done! ${results}`)) 
  7.   .catch(console.error) 
  8.   .finally(() => console.timeEnd('promise.race')); 

输出是什么?

输出 b。使用 Promise.race,最先执行完成就会结果最后的返回结果。

你可能会问:Promise.race的用途是什么?

我没胡经常使用它。但是,在某些情况下,它可以派上用场,比如计时请求或批量处理请求数组。

  1. Promise.race([ 
  2.   fetch('http://slowwly.robertomurray.co.uk/delay/3000/url/https://api.jsonbin.io/b/5d1fb4dd138da811182c69af'), 
  3.   new Promise((resolve, reject) => setTimeout(() => reject(new Error('request timeout')), 1000)) 
  4. ]).then(console.log) 
  5. .catch(console.error); 

如果请求足够快,那么就会得到请求的结果。

 

1.5 Promise 常见问题
串行执行 promise 并传递参数

这次,我们将对Node的fs使用promises API,并将两个文件连接起来:

  1. const fs = require('fs').promises; // requires node v8+ 
  2. fs.readFile('file.txt''utf8'
  3.   .then(content1 => fs.writeFile('output.txt', content1)) 
  4.   .then(() => fs.readFile('file2.txt''utf8')) 
  5.   .then(content2 => fs.writeFile('output.txt', content2, { flag: 'a+' })) 
  6.   .catch(error => console.log(error)); 

在此示例中,我们读取文件1并将其写入output 文件。稍后,我们读取文件2并将其再次附加到output文件。如你所见,writeFile promise返回文件的内容,你可以在下一个then子句中使用它。

如何链接多个条件承诺?
你可能想要跳过 Promise 链上的特定步骤。有两种方法可以做到这一点。

  1. const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3)); 
  2. const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 2e3)); 
  3. const c = () => new Promise((resolve) => setTimeout(() => { console.log('c'), resolve() }, 3e3)); 
  4. const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 4e3)); 
  5. const shouldExecA = true
  6. const shouldExecB = false
  7. const shouldExecC = false
  8. const shouldExecD = true
  9. Promise.resolve()  .then(() => shouldExecA && a()) 
  10.   .then(() => shouldExecB && b()) 
  11.   .then(() => shouldExecC && c()) 
  12.   .then(() => shouldExecD && d()) 
  13.   .then(() => console.log('done')) 

如果你运行该代码示例,你会注意到只有a和d被按预期执行。

另一种方法是创建一个链,然后仅在以下情况下添加它们:

  1. const chain = Promise.resolve(); 
  2. if (shouldExecA) chain = chain.then(a); 
  3. if (shouldExecB) chain = chain.then(b); 
  4. if (shouldExecC) chain = chain.then(c); 
  5. if (shouldExecD) chain = chain.then(d); 
  6. chain  .then(() => console.log('done')); 

如何限制并行 Promise?
要做到这一点,我们需要以某种方式限制Promise.all。

假设你有许多并发请求要执行。如果使用 Promise.all 是不好的(特别是在API受到速率限制时)。因此,我们需要一个方法来限制 Promise 个数, 我们称其为promiseAllThrottled。

  1. // simulate 10 async tasks that takes 5 seconds to complete. 
  2. const requests = Array(10) 
  3.   .fill()  .map((_, i) => () => new Promise((resolve => setTimeout(() => { console.log(`exec'ing task #${i}`), resolve(`task #${i}`); }, 5000)))); 
  4. promiseAllThrottled(requests, { concurrency: 3 }) 
  5.   .then(console.log) 
  6.   .catch(error => console.error('Oops something went wrong', error)); 

输出应该是这样的:

以上代码将并发限制为并行执行的3个任务。

实现promiseAllThrottled 一种方法是使用Promise.race来限制给定时间的活动任务数量。

  1. /** 
  2.  * Similar to Promise.all but a concurrency limit 
  3.  * 
  4.  * @param {Array} iterable Array of functions that returns a promise 
  5.  * @param {Object} concurrency max number of parallel promises running 
  6.  */ 
  7. function promiseAllThrottled(iterable, { concurrency = 3 } = {}) { 
  8.   const promises = []; 
  9.   function enqueue(current = 0, queue = []) { 
  10.     // return if done 
  11.     if (current === iterable.length) { return Promise.resolve(); } 
  12.     // take one promise from collection 
  13.     const promise = iterable[current]; 
  14.     const activatedPromise = promise(); 
  15.     // add promise to the final result array 
  16.     promises.push(activatedPromise); 
  17.     // add current activated promise to queue and remove it when done 
  18.     const autoRemovePromise = activatedPromise.then(() => { 
  19.       // remove promise from the queue when done 
  20.       return queue.splice(queue.indexOf(autoRemovePromise), 1); 
  21.     }); 
  22.     // add promise to the queue 
  23.     queue.push(autoRemovePromise); 
  24.     // if queue length >= concurrency, wait for one promise to finish before adding more. 
  25.     const readyForMore = queue.length < concurrency ? Promise.resolve() : Promise.race(queue); 
  26.     return readyForMore.then(() => enqueue(current + 1, queue)); 
  27.   } 
  28.   return enqueue() 
  29.     .then(() => Promise.all(promises)); 

promiseAllThrottled一对一地处理 Promises 。它执行Promises并将其添加到队列中。如果队列小于并发限制,它将继续添加到队列中。达到限制后,我们使用Promise.race等待一个承诺完成,因此可以将其替换为新的承诺。这里的技巧是,promise 自动完成后会自动从队列中删除。另外,我们使用 race 来检测promise 何时完成,并添加新的 promise 。

 

责任编辑:姜华 来源: 今日头条
相关推荐

2020-07-29 08:26:40

Webpack前端模块

2021-05-06 09:00:00

JavaScript静态代码开发

2024-01-12 14:37:29

智能家居人工智能

2022-04-24 15:21:01

MarkdownHTML

2020-12-15 14:05:15

云计算

2010-06-13 11:13:38

UML初学者指南

2022-07-22 13:14:57

TypeScript指南

2022-10-10 15:28:45

负载均衡

2023-07-03 15:05:07

预测分析大数据

2021-05-10 08:50:32

网络管理网络网络性能

2023-07-28 07:31:52

JavaScriptasyncawait

2022-03-28 09:52:42

JavaScript语言

2022-09-05 15:36:39

Linux日志记录syslogd

2010-08-26 15:47:09

vsftpd安装

2018-10-28 16:14:55

Reactreact.js前端

2023-02-10 08:37:28

2012-03-14 10:56:23

web app

2018-05-14 08:53:51

Linux命令shuf

2014-04-01 10:20:00

开源Rails

2023-02-19 15:31:09

架构软件开发代码
点赞
收藏

51CTO技术栈公众号