百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

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

liuian 2025-03-25 15:21 25 浏览

这篇文章算是 JavaScript Promises 比较全面的教程,该文介绍了必要的方法,例如 then,catch和finally。此外,还包括处理更复杂的情况,例如与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 构造函数接受一个回调,带有两个参数resolve和reject。

  • 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。

一旦你调用一种方法(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方法可以让异步操作成功或失败时得到通知。它包含两个参数,一个用于成功执行,另一个则在发生错误时使用。

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)

输出如下:


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

我们可以使用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)部分的错误,因此未调用catch。d不会被调用。如果要忽略错误并继续执行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'))

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

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

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.racequeue return> enqueue(current + 1, queue));
  }

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

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

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


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

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

相关推荐

总结下SpringData JPA 的常用语法

SpringDataJPA常用有两种写法,一个是用Jpa自带方法进行CRUD,适合简单查询场景、例如查询全部数据、根据某个字段查询,根据某字段排序等等。另一种是使用注解方式,@Query、@Modi...

解决JPA在多线程中事务无法生效的问题

在使用SpringBoot2.x和JPA的过程中,如果在多线程环境下发现查询方法(如@Query或findAll)以及事务(如@Transactional)无法生效,通常是由于S...

PostgreSQL系列(一):数据类型和基本类型转换

自从厂子里出来后,数据库的主力就从Oracle变成MySQL了。有一说一哈,贵确实是有贵的道理,不是开源能比的。后面的工作里面基本上就是主MySQL,辅MongoDB、ES等NoSQL。最近想写一点跟...

基于MCP实现text2sql

目的:基于MCP实现text2sql能力参考:https://blog.csdn.net/hacker_Lees/article/details/146426392服务端#选用开源的MySQLMCP...

ORACLE 错误代码及解决办法

ORA-00001:违反唯一约束条件(.)错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。ORA-00017:请求会话以设置跟踪事件ORA-00018:超出最大会话数ORA-00...

从 SQLite 到 DuckDB:查询快 5 倍,存储减少 80%

作者丨Trace译者丨明知山策划丨李冬梅Trace从一开始就使用SQLite将所有数据存储在用户设备上。这是一个非常不错的选择——SQLite高度可靠,并且多种编程语言都提供了广泛支持...

010:通过 MCP PostgreSQL 安全访问数据

项目简介提供对PostgreSQL数据库的只读访问功能。该服务器允许大型语言模型(LLMs)检查数据库的模式结构,并执行只读查询操作。核心功能提供对PostgreSQL数据库的只读访问允许L...

发现了一个好用且免费的SQL数据库工具(DBeaver)

缘起最近Ai不是大火么,想着自己也弄一些开源的框架来捣腾一下。手上用着Mac,但Mac都没有显卡的,对于学习Ai训练模型不方便,所以最近新购入了一台4090的拯救者,打算用来好好学习一下Ai(呸,以上...

微软发布.NET 10首个预览版:JIT编译器再进化、跨平台开发更流畅

IT之家2月26日消息,微软.NET团队昨日(2月25日)发布博文,宣布推出.NET10首个预览版更新,重点改进.NETRuntime、SDK、libraries、C#、AS...

数据库管理工具Navicat Premium最新版发布啦

管理多个数据库要么需要使用多个客户端应用程序,要么找到一个可以容纳你使用的所有数据库的应用程序。其中一个工具是NavicatPremium。它不仅支持大多数主要的数据库管理系统(DBMS),而且它...

50+AI新品齐发,微软Build放大招:拥抱Agent胜算几何?

北京时间5月20日凌晨,如果你打开微软Build2025开发者大会的直播,最先吸引你的可能不是一场原本属于AI和开发者的技术盛会,而是开场不久后的尴尬一幕:一边是几位微软员工在台下大...

揭秘:一条SQL语句的执行过程是怎么样的?

数据库系统能够接受SQL语句,并返回数据查询的结果,或者对数据库中的数据进行修改,可以说几乎每个程序员都使用过它。而MySQL又是目前使用最广泛的数据库。所以,解析一下MySQL编译并执行...

各家sql工具,都闹过哪些乐子?

相信这些sql工具,大家都不陌生吧,它们在业内绝对算得上第一梯队的产品了,但是你知道,他们都闹过什么乐子吗?首先登场的是Navicat,这款强大的数据库管理工具,曾经让一位程序员朋友“火”了一把。Na...

详解PG数据库管理工具--pgadmin工具、安装部署及相关功能

概述今天主要介绍一下PG数据库管理工具--pgadmin,一起来看看吧~一、介绍pgAdmin4是一款为PostgreSQL设计的可靠和全面的数据库设计和管理软件,它允许连接到特定的数据库,创建表和...

Enpass for Mac(跨平台密码管理软件)

还在寻找密码管理软件吗?密码管理软件有很多,但是综合素质相当优秀且完全免费的密码管理软件却并不常见,EnpassMac版是一款免费跨平台密码管理软件,可以通过这款软件高效安全的保护密码文件,而且可以...