Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
husky-dot opened this issue Sep 9, 2020 · 0 comments
Open

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

husky-dot opened this issue Sep 9, 2020 · 0 comments

Comments

@husky-dot
Copy link
Owner

作者:Adrian Mejia
译者:前端小智
来源:adrianmjia

点赞再看,养成习惯

本文 GitHub https://github.com/qq449245884/xiaozhi 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

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

1.JavaScript Promises

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

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

  • 让异步代码更易于阅读。
  • 提供组合错误处理。
    * 更好的流程控制,可以让异步并行或串行执行。

回调更容易形成深度嵌套的结构(也称为回调地狱)。 如下所示:

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

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

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error);

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

1.1 如何将现有的回调 API 转换为 Promise?

我们可以使用 Promise 构造函数将回调转换为 Promise。

Promise 构造函数接受一个回调,带有两个参数resolvereject

  • Resolve:是在异步操作完成时应调用的回调。
  • Reject:是发生错误时要调用的回调函数。

构造函数立即返回一个对象,即 Promise 实例。 当在 promise 实例中使用.then方法时,可以在Promise “完成” 时得到通知。 让我们来看一个例子。

Promise 仅仅只是回调?

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

Promise 反面模式(Promises 地狱)

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

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

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

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

a()
  .then(b)
  .then(c)
  .then(d)

超时

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

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('time is up ⏰');
  }, 1e3);

  setTimeout(() => {
    reject('Oops 🔥');
  }, 2e3);
});

promise
  .then(console.log)
  .catch(console.error);

是输出:

time is up ⏰
Oops! 🔥

还是输出:

time is up ⏰

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

一旦你调用一种方法(resolvereject),另一种方法就会失效,因为 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 公开了三个主要方法:thencatchfinally。 我们逐一配合事例探讨一下。

Promise then

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

promise.then(onSuccess, onError);

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

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

Promise 链

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

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

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

then#1
then#2
then#3

Promise catch

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

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

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

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

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)

输出如下:

在这种情况下,可以看到abc上的错误消息。

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

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

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

Promise.resolve()
  .then(a)
  .then(b)
  .then(() => c().catch(() => console.log('error ignored')))
  .then(d)
  .catch(console.error)

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

Promise finally

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

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

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .then(() => console.log('always called'));

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

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .finally(() => console.log('always called'));

1.4 Promise 类方法

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

  • Promise.all
  • Promise.reject
  • Promise.resolve
  • Promise.race

Promise.resolve 和 Promise.reject

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

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

上面会输出 Yay!!!

Promise.reject('Oops 🔥')
  .then(console.log)
  .catch(console.error)

使用 Promise.all 并行执行多个 Promise

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

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

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

const axios = require('axios');

const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets');
const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD');
const currency = 'EUR';

// Get the price of bitcoins on
Promise.all([bitcoinPromise, dollarPromise])
  .then(([bitcoinMarkets, dollarExchanges]) => {
    const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD';
    const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc)
    const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price;
    const rate = dollarExchanges.data.rates[currency];
    return rate * coinbaseBtcInUsd;
  })
  .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`))
  .catch(console.log);

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

我们再举一个例子:

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.all');
Promise.all([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.all'));

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

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

Promise race

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

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.race');
Promise.race([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.race'));

输出是什么?

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

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

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

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


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

1.5 Promise 常见问题

串行执行 promise 并传递参数

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

const fs = require('fs').promises; // requires node v8+

fs.readFile('file.txt', 'utf8')
  .then(content1 => fs.writeFile('output.txt', content1))
  .then(() => fs.readFile('file2.txt', 'utf8'))
  .then(content2 => fs.writeFile('output.txt', content2, { flag: 'a+' }))
  .catch(error => console.log(error));

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

如何链接多个条件承诺?

你可能想要跳过 Promise 链上的特定步骤。有两种方法可以做到这一点。

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

const shouldExecA = true;
const shouldExecB = false;
const shouldExecC = false;
const shouldExecD = true;

Promise.resolve()
  .then(() => shouldExecA && a())
  .then(() => shouldExecB && b())
  .then(() => shouldExecC && c())
  .then(() => shouldExecD && d())
  .then(() => console.log('done'))

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

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

const chain = Promise.resolve();

if (shouldExecA) chain = chain.then(a);
if (shouldExecB) chain = chain.then(b);
if (shouldExecC) chain = chain.then(c);
if (shouldExecD) chain = chain.then(d);

chain
  .then(() => console.log('done'));

如何限制并行 Promise?

要做到这一点,我们需要以某种方式限制Promise.all

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

// simulate 10 async tasks that takes 5 seconds to complete.
const requests = Array(10)
  .fill()
  .map((_, i) => () => new Promise((resolve => setTimeout(() => { console.log(`exec'ing task #${i}`), resolve(`task #${i}`); }, 5000))));

promiseAllThrottled(requests, { concurrency: 3 })
  .then(console.log)
  .catch(error => console.error('Oops something went wrong', error));

输出应该是这样的:

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

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

/**
 * Similar to Promise.all but a concurrency limit
 *
 * @param {Array} iterable Array of functions that returns a promise
 * @param {Object} concurrency max number of parallel promises running
 */
function promiseAllThrottled(iterable, { concurrency = 3 } = {}) {
  const promises = [];

  function enqueue(current = 0, queue = []) {
    // return if done
    if (current === iterable.length) { return Promise.resolve(); }
    // take one promise from collection
    const promise = iterable[current];
    const activatedPromise = promise();
    // add promise to the final result array
    promises.push(activatedPromise);
    // add current activated promise to queue and remove it when done
    const autoRemovePromise = activatedPromise.then(() => {
      // remove promise from the queue when done
      return queue.splice(queue.indexOf(autoRemovePromise), 1);
    });
    // add promise to the queue
    queue.push(autoRemovePromise);

    // if queue length >= concurrency, wait for one promise to finish before adding more.
    const readyForMore = queue.length < concurrency ? Promise.resolve() : Promise.race(queue);
    return readyForMore.then(() => enqueue(current + 1, queue));
  }

  return enqueue()
    .then(() => Promise.all(promises));
}

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

人才们的 【三连】 就是小智不断分享的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言,最后,谢谢大家的观看。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://adrianmejia.com/promises-tutorial-concurrency-in-javascript-node/

文章每周持续更新,可以微信搜索 【大迁世界 】 第一时间阅读,回复 【福利】 有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant