- promise 是一个表示稍后可用的结果的对象。
- 它会一直等待,直到满足或拒绝为止,并且一旦解决就永远不会改变。
- 每个
then返回一个新的 Promise,这使得链接成为可能。 - 当需要每个结果时使用
Promise.all,当您想要每个结果时使用Promise.allSettled。
网络请求、文件读取和计时器都会稍后完成,但 JavaScript 不能简单地停止和等待,否则页面会冻结。 JavaScript Promise 为您提供了一种简洁的方式来表达“当此操作完成时,执行此操作,如果失败,则执行此操作”。
即使您主要编写 async 和 await,您也会在下面使用 Promise,因此理解它们会使调试变得更加容易。我们在 Node.js 24 上运行这些示例,显示的输出来自该运行。它们在现代浏览器中的行为方式相同。
什么是承诺?
承诺是未来值的占位符。想象一下当你点餐时餐馆递给你的蜂鸣器。你还没有拿到食物,但你拿着一些东西,它会告诉你食物何时准备好,或者有问题,同时你可以自由地做其他事情。
在代码中,异步函数立即返回一个承诺,然后您附加接下来应该发生的事情:
fetch("/api/user")
.then((response) => response.json())
.then((user) => console.log(user.name))
.catch((error) => console.log("Failed:", error.message));
三个承诺状态
| 状态 | 含义 | 运行什么 |
|---|---|---|
| 待定 | 工作还没有完成 | 还没有什么 |
| 实现了 | 它以一个值结束 | then 处理程序 |
| 被拒绝 | 失败是有原因的 | catch 处理程序 |
履行或拒绝的承诺称为已解决。承诺只能解决一次。之后,它的状态和值就固定了,以后对 resolve 或 reject 的调用将被忽略。
创造一个承诺
您通常会使用 fetch 等 API 中的 Promise,但创建一个 Promise 会显示它们是如何工作的。构造函数接收两个函数,resolve 和 reject:
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Asha" });
} else {
reject(new Error("Invalid id"));
}
}, 100);
});
}
console.log(fetchUser(1));
// Promise { <pending> }
立即记录承诺表明它仍处于待处理状态,因为计时器尚未触发。该值只能通过处理程序或 await 访问。始终使用 Error 对象而不是字符串来拒绝,因为 Error 带有有助于调试的堆栈跟踪。
然后,抓住,最后
这三个方法将处理程序附加到 Promise:
fetchUser(1).then((user) => console.log(user.name));
// Asha
fetchUser(-1)
.then((user) => console.log("never runs", user))
.catch((error) => console.log("caught:", error.message))
.finally(() => console.log("finally"));
// caught: Invalid id
// finally
then 成功时运行,catch 失败时运行,finally 两种情况都运行。这使得 finally 成为清理的正确位置,例如隐藏加载旋转器。它不会收到任何争论,因为它不应该关心结果。
连锁承诺
关键思想是 then 总是返回一个 新的 Promise。无论你的处理程序返回什么,都会成为新承诺的值:
Promise.resolve(2)
.then((n) => n * 10)
.then((n) => n + 1)
.then((n) => console.log("chain:", n));
// chain: 21
如果处理程序返回另一个 Promise,链会等待它稳定下来,然后再继续。这就是将嵌套回调转变为扁平步骤序列的原因:
getUser(id)
.then((user) => getOrders(user.id))
.then((orders) => getInvoice(orders[0].id))
.then((invoice) => console.log(invoice.total))
.catch(handleError);
错误如何通过链条传播
被拒绝的 Promise 会跳过每个 then,直到到达 catch。将其扔进处理程序内也算作拒绝。正常返回的 catch 使链回到正轨:
Promise.resolve("start")
.then(() => {
throw new Error("step 2 failed");
})
.then(() => console.log("skipped"))
.catch((error) => {
console.log("caught:", error.message);
return "recovered";
})
.then((value) => console.log("continues:", value));
// caught: step 2 failed
// continues: recovered
将此视为 try 和 catch 的异步版本。末尾的一个 catch 通常覆盖整个链。如果您只想记录错误但仍然失败,请从 catch 内部再次抛出它。
提示: fetch 仅因网络故障而拒绝。 404 或 500 响应仍然满足,因此请自行检查 response.ok。我们的 HTTP 状态代码备忘单 解释了每个代码的含义。
Promise.all、allSettled、race 和 any
这些静态方法同时协调多个 Promise。它们都接受一个数组并返回一个承诺。
| 方法 | 满足时 | 拒绝时 | 用它来 |
|---|---|---|---|
Promise.all |
全部满足 | 任何人都拒绝 | 所有结果均为必填项 |
Promise.allSettled |
一切安定 | 从来没有 | 你想要每一个结果 |
Promise.race |
第一个结算满足 | 第一个解决拒绝问题 | 超时 |
Promise.any |
第一个满足 | 全部拒绝 | 第一个成功的源码 |
为了测试它们,我们使用了两个小助手,一个在延迟后成功,另一个在延迟后失败:
const wait = (ms, value) =>
new Promise((resolve) => setTimeout(() => resolve(value), ms));
const fail = (ms, message) =>
new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
Promise.all
await Promise.all([wait(300, "a"), wait(100, "b"), wait(200, "c")]);
// [ 'a', 'b', 'c' ] took about 300 ms
有两个细节需要注意。结果保持输入顺序,即使 b 首先完成。总时间约为 300 毫秒,这是最慢任务的时间,而不是 600 毫秒的总和,因为这三个任务已经同时运行。如果其中任何一个拒绝,整个事情就会立即拒绝:
await Promise.all([wait(100, "a"), fail(50, "b failed")]);
// rejected: b failed
Promise.allSettled
这会等待一切并报告每个结果,因此一次失败不会丢失其余的结果。它适合每个小部件独立加载的仪表板:
const results = await Promise.allSettled([wait(50, "a"), fail(50, "b failed")]);
results.map((r) => r.status);
// [ 'fulfilled', 'rejected' ]
Promise.race 和 Promise.any
await Promise.race([wait(300, "slow"), wait(100, "fast")]); // fast
await Promise.any([fail(50, "x"), wait(100, "ok")]); // ok
不同之处在于,race 会先解决,即使它失败了,而 any 只要成功,就会忽略失败。 race 的一个经典用法是为缓慢的请求添加超时,尽管较慢的任务会继续在后台运行,除非您取消它。
Promise 和 async/await
async 和 await 是使用相同 Promise 的更易读的方式。 async 函数总是返回一个 Promise,而 await 会暂停该函数,直到 Promise 解决:
async function showInvoice(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
console.log(orders.length);
} catch (error) {
console.log("Failed:", error.message);
}
}
拒绝会变成抛出错误,因此普通的 try 和 catch 可以工作。您仍然需要 Promise.all 来进行并行工作,因为等待一个又一个调用会按顺序运行它们。我们的 JavaScript async/await 指南对此进行了更深入的探讨。
Promise 和事件循环
即使承诺已经解决,承诺处理程序也不会立即被调用。它们在当前代码完成之后、任何计时器之前运行。这是证明:
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");
// sync
// microtask
// timeout
同步代码首先运行。接下来是微任务队列,它保存 Promise 处理程序。只有这样,事件循环才会开始执行下一个任务,例如计时器。这是一个流行的面试问题,它解释了为什么 setTimeout 为零并不是真正立竿见影的。
Promise 与回调
| 关注 | 回调 | 承诺 |
|---|---|---|
| 测序 | 嵌套,每一步都更深 | 扁平链条 |
| 错误 | 每个级别都经过检查 | 一根catch即可覆盖链条 |
| 并行工作 | 手动计数器 | Promise.all |
| 误拨两次电话? | 可能 | 不能两次结算 |
常见错误
- 忘记返回内部
then。如果没有return,下一步将收到undefined,并且不会等待您内心的承诺。 - 根本没有
catch。 这会导致未处理的拒绝,并且在 Node.js 中默认停止该进程。 - 嵌套而不是链接。将
then放入then中会重新创建回调地狱。返回承诺并继续链条。 - 在
new Promise中包装一个promise。如果一个函数已经返回一个promise,那么就使用它。 - 将
forEach与异步回调结合使用。它不会等待。将for...of或Promise.all与map结合使用,如我们的 JavaScript 数组方法指南 中所示。 - 认为 Promise 可以取消。 它本身不能取消。对于请求,请使用
AbortController。
最后的话
Promise 归结为一些规则:promise 一次解决,then 返回一个新的 Promise,错误沿着链向下传播,直到有东西捕获它们。一旦这些规则感觉很自然,async 和 await 就不再那么神奇了。为了练习,使用 Promise 重写一个基于回调的函数,然后使用 Promise.all 获取两个端点并正确处理失败。
常见问题
JavaScript 中的 Promise 是什么?
Promise 是一个代表异步工作的最终结果的对象。它开始时处于待处理状态,然后要么满足一个值,要么拒绝一个原因。
then 和 catch 有什么区别?
then 在承诺履行时运行,catch 在拒绝时运行。 catch 是有效的,然后只使用拒绝处理程序,并且它还可以捕获在前面的步骤中抛出的错误。
Promise.all 和 Promise.allSettled 之间有什么区别?
一旦一个 Promise 被拒绝,Promise.all 就会拒绝。 Promise.allSettled 始终等待所有这些并报告每个的状态和值或原因。
我应该使用 Promise 还是 async/await?
他们一起工作,因为等待是建立在承诺之上的。大多数代码使用 async/await 读起来效果最好,而 Promise.all 和类似的方法对于并行工作仍然有用。
Promise 是否并行运行?
JavaScript 在一个线程上一次运行一段代码,但像网络请求一样的工作是在后台发生的。其中几个可以同时进行。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL CASE WHEN 语句:语法、实例和常见错误
SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。
阅读文章 →软件开发什么是智威汤逊?通过工作示例解释 JSON Web 令牌
什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。
阅读文章 →软件开发JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例
JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。
阅读文章 →软件开发Python 生成器和 Yield 解释:通过真实示例节省内存
Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。
阅读文章 →人工智能什么是嵌入?向量和余弦相似度简单解释
什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。
阅读文章 →DevOps蓝绿部署与金丝雀部署:差异、权衡以及如何选择
蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。
阅读文章 →测试自动化不稳定的测试:造成这些问题的原因以及如何彻底解决它们
我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。
阅读文章 →软件测试负载测试与压力测试:差异、指标和 k6 示例
负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL UNION 与 UNION ALL:差异、示例和性能测试
SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。
阅读文章 →软件开发JavaScript Fetch API 教程:GET、POST、错误处理和超时
针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。
阅读文章 →软件开发Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例
使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。
阅读文章 →软件开发Python 类和对象:带有实际示例的 OOP 教程
用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。
阅读文章 →人工智能什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释
什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。
阅读文章 →DevOps适合初学者的 Linux 命令:带有示例的基本终端指南
适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。
阅读文章 →测试自动化Selenium 与 Playwright:主要区别、代码比较以及选择
Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git 命令备忘单:您实际使用的命令,以及示例
Git 命令备忘单按任务分组:设置、暂存、分支、远程、存储、历史记录和撤消错误,以及来自真实演示存储库的输出。
阅读文章 →软件开发JavaScript Promise 解释:then、catch、Promise.all 和常见错误
JavaScript 承诺用我们运行的代码进行了解释:状态、then 和 catch 链、错误处理、Promise.all、allSettled、race 和 any、事件循环和常见错误。
阅读文章 →软件开发Python 字典教程:方法、示例和常见模式
Python 字典教程,包含我们运行的代码:创建、读取和更新字典、关键方法、合并、理解、计数和分组模式以及常见错误。
阅读文章 →人工智能什么是 RAG?通过工作示例解释检索增强生成
什么是RAG?检索增强生成逐步解释:索引、分块、嵌入、检索、一个小型的 Python 工作示例、评估和常见故障。
阅读文章 →DevOpsTerraform 初学者教程:从第一个文件到模块的基础设施即代码
适合初学者的 Terraform 教程:从没有云帐户开始,学习初始化、计划和应用,然后是变量、输出、状态、模块和安全的团队工作流程。
阅读文章 →DevOpsDocker 与 Kubernetes:主要区别、它们如何协同工作以及您需要哪些
Docker 与 Kubernetes 的解释:各自的作用、同一个应用程序同时运行、Docker Compose 和 Swarm 的比较、dockershim 的变化以及如何决定您需要什么。
阅读文章 →测试自动化Pytest 初学者教程:夹具、参数化和真实示例
面向初学者的 pytest 教程,其中包含我们在 pytest 9.2 上运行的测试:断言、固定装置、参数化、conftest、标记、tmp_path、monkeypatch、有用的标志和 CI。
阅读文章 →软件测试软件测试生命周期 (STLC):6 个阶段、进入和退出标准、示例
软件测试生命周期解释:六个 STLC 阶段,包括进入和退出标准、可交付成果、有效的登录功能示例、STLC 与 SDLC 以及敏捷中的 STLC。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →