Menu

JavaScript Promise 教程:then、catch 与 Promise.all

JavaScript 中 Promise 的工作原理:三种状态,用 then 和 catch 链式调用,用 Promise.all 组合,以及用 new Promise 自己创建。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Promise 是未来值的占位符

当 JavaScript 需要做一件耗时的事,比如网络请求、读取文件或等待定时器,它无法立刻把结果交给你。它会先交给你一个 Promise:一个代表“最终会存在”的值的对象。

第一个 console.log 显示的是一个 pending 状态的 Promise。半秒后,Promise 被解决,.then 回调带着这个值运行。Promise 本身只是一个对象;它的妙处在于,值到达时它知道如何通知所有正在等待的人。

三种状态

Promise 始终处于以下三种状态之一:

  • pending:工作正在进行,还没有值。
  • fulfilled:工作成功,值已经可用。
  • rejected:工作失败,有一个错误。

Promise 从 pending 转变为 fulfilled 或 rejected,只会发生一次,之后就永远保持在那个状态。你无法撤销一个已解决的 Promise,也无法解决它两次。

Promise.resolve(value) 创建一个已兑现的 Promise;Promise.reject(error) 创建一个已拒绝的 Promise。写测试时很方便,函数有时能立刻给出答案、却需要返回 Promise 时也很好用。

读取值:.then 和 .catch

你不能直接拆开一个 Promise,而是把回调传给 .then,值准备好时 Promise 会调用它:

如果 Promise 被拒绝,.catch(fn) 就会运行。在底层,它是 .then(undefined, fn) 的简写。链末尾的一个 .catch() 能处理它上面任何一步的拒绝,不需要在每个 .then 后面都加一个。

链式调用:每个 .then 都返回一个新的 Promise

这是最容易让人困惑的地方。.then() 不只是运行一个回调,它还会返回一个新的 Promise,这个 Promise 会以回调的返回值解决。这样就可以链式调用:

每一步的结果都传给下一步。如果 .then 回调返回的是一个 Promise,链会等这个 Promise 完成后再继续,所以异步步骤可以干净地组合起来:

三个依次执行的异步步骤,没有任何嵌套。把同样的逻辑用回调写一遍对比一下,你就会明白 Promise 为什么会流行。

错误会沿着链往下传

被拒绝的 Promise 会跳过每一个 .then,直到遇到 .catch。这就是整个错误处理模型:

在 .then 里抛出错误,会让这个 .then 返回的 Promise 被拒绝。下一个 .then 看到拒绝后会继续往下传,直到被 .catch 接住。通常链末尾一个 .catch 就够了。而完全没有 .catch 的链会产生 “unhandled promise rejection” 警告,这是需要修复的。

用 new Promise 自己创建

大多数时候,你使用的是库提供的 Promise。偶尔你需要包装一个不返回 Promise 的东西,通常是旧式的回调 API:

传给 new Promise 的函数叫作“执行器”(executor)。它接收两个参数:resolve(用成功的值调用它)和 reject(用错误调用它)。两者只调用其中一个,并且只调用一次。之后的调用都会被忽略。

两个能省去麻烦的习惯:

  • 只有在包装还不是基于 Promise 的东西时,才用 new Promise。如果一个函数已经返回 Promise,直接返回它就行。
  • 始终用 Error 对象来 reject,而不是字符串。堆栈跟踪值得保留。

并行执行:Promise.all

.then 链是按顺序执行的。当你有几个互不依赖的异步任务,想让它们同时运行时,就用 Promise.all:

三个定时器并发运行。等每个 Promise 都兑现后,Promise.all 会以一个结果数组解决,顺序与输入相同。总耗时大约是 400ms,而不是 900ms。

要注意的是:只要其中任何一个 Promise 被拒绝,Promise.all 就会立刻拒绝,其他结果也就丢失了。当你需要所有结果时(例如渲染一个依赖三次 API 调用的页面),这正是正确的行为。如果不需要,就用 allSettled。

允许部分失败:Promise.allSettled

Promise.allSettled 会等待每个 Promise 完成,无论兑现还是拒绝,然后给你一份报告:

每个结果都是一个对象:{ status: "fulfilled", value } 或 { status: "rejected", reason }。在部分成功也可以接受的场景中很有用,比如记录一批事件、获取一堆缩略图、运行互不相关的健康检查。

还有两个值得了解的组合方法:

  • Promise.race([...]):只要第一个 Promise 完成(无论成败)就随之完成。适合实现超时。
  • Promise.any([...]):以第一个成功的结果兑现,忽略拒绝。只有所有 Promise 都被拒绝时才会拒绝。

Promise 永远是异步的

即使是一个已经解决的 Promise,它的 .then 回调也是异步调用的,绝不会同步执行,也不会在同一轮中执行:

输出是 before、after、immediate。.then 回调会等当前代码执行完毕,再在微任务队列中运行。“Promise 回调永远不会同步执行”这条规则,正是 Promise 与同步代码混用时行为可预测的原因:同步代码总是先执行完。

下一篇:async/await

链式调用 .then 能用,但一旦超过两三步,代码就开始像一段楼梯。async/await 是建立在 Promise 之上的语法,让你像写同步代码一样写同样的逻辑:用 try/catch 处理错误,用普通变量保存中间值。这就是下一篇的内容。

常见问题

JavaScript 中的 Promise 是什么?

Promise 是一个对象,代表一个还没准备好的值,通常是网络请求这类异步操作将来的结果。它始终处于三种状态之一:pending、fulfilled 或 rejected。你通过 .then() 和 .catch() 挂上回调,来读取最终的值。

then 和 catch 有什么区别?

.then(onFulfilled) 在 Promise 成功解决时运行,接收解决后的值。.catch(onRejected) 在 Promise(或链中更早的任何 Promise)被拒绝时运行,接收错误。链末尾的一个 .catch() 就能处理它上面任何一步的失败。

Promise.all 有什么作用?

Promise.all([p1, p2, p3]) 接收一个 Promise 数组,返回一个新的 Promise,它会以所有解决值组成的数组兑现,但前提是每个输入的 Promise 都已兑现。只要其中任何一个被拒绝,整体就会立刻被拒绝。如果无论成败都想拿到每个结果,就用 Promise.allSettled。

应该用 Promise 还是 async/await?

它们是同一套机制:async/await 是建立在 Promise 之上的语法。大多数新代码用 async/await 写更易读,但你仍然返回 Promise,仍然用 try/catch 或 .catch() 捕获错误,仍然用 Promise.all 并行执行任务。理解了 Promise 的原理,async/await 也就顺理成章了。

Coddy 编程语言插图

和 Coddy 一起学编程

开始