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 也就顺理成章了。