先记住这个答案
下一个 then 拿到的是值,不是 Promise。then 每次调用都会生成一个新的 Promise,回调返回普通值时,这个新 Promise 直接以该值成功;回调返回一个 Promise 时,新 Promise 会先等着,等内层 Promise 完成后跟随它的状态:内层成功,外层就以同样的值成功;内层失败,外层就以同样的原因失败。这个过程叫展开,也是 Promise 链能把多个异步操作排成一条直线、不用层层嵌套的原因。
then返回 Promise,下一步拿到的是它的最终值- 内层失败,外层也失败,错误往后传
- 带
then方法的对象也会被当成 Promise 展开 - 回调返回自己所在的 Promise 会报
TypeError
返回 Promise 时链会等它
最常见的场景是先请求用户信息,再用结果请求订单。第二个请求在 then 里 return 出去,下一个 then 就能直接拿到订单数据。
function delay(ms, value) {
return new Promise((resolve) => setTimeout(() => resolve(value), ms))
}
Promise.resolve('start')
.then(() => delay(100, '用户数据')) // 返回一个 100 毫秒后才完成的 Promise
.then((v) => {
console.log('拿到:', v, typeof v) // 拿到的是字符串,不是 Promise
return Promise.reject(new Error('权限不足')) // 返回一个失败的 Promise
})
.then(() => console.log('不会执行'))
.catch((err) => console.log('catch 到:', err.message))查看输出与解释
拿到: 用户数据 string
catch 到: 权限不足第二个 then 的回调在 100 毫秒后才执行,说明链确实在等 delay 返回的 Promise。第二个 then 返回了一个失败的 Promise,外层也跟着失败,跳过了中间的 then,直接进 catch。
如果忘了写 return,比如写成 .then(() => { delay(100, '用户数据') }),回调返回的是 undefined,外层 Promise 立刻以 undefined 成功,链不会等这个请求,下一步拿到的也是 undefined。这是 Promise 链里最常见的 bug。
展开是怎么判断的
外层 Promise 拿到回调的返回值后,会检查它是不是一个 thenable,也就是带有 then 方法的对象。是的话,就调用它的 then,把自己的 resolve 和 reject 传进去,让对方来决定自己的结果。所以展开不要求返回值是原生 Promise,只要长得像就行。
const p = Promise.resolve(1)
console.log(Promise.resolve(p) === p) // 传入原生 Promise,直接原样返回
const thenable = {
then(onFulfilled) {
console.log('then 被调用')
onFulfilled(42)
}
}
Promise.resolve()
.then(() => thenable)
.then((v) => console.log('展开得到:', v))查看输出与解释
true
then 被调用
展开得到: 42这个设计是为了兼容。ES6 之前社区有很多 Promise 库,比如 Bluebird、Q,它们的实例都不是原生 Promise,但都有 then 方法。按 thenable 判断,这些库的对象可以直接混进原生 Promise 链里用。
- 展开是递归的
- 如果内层 Promise 完成的值本身又是一个 Promise,外层会继续等下去,直到拿到一个不是 thenable 的值为止。所以不管套多少层,最后到手的一定是普通值。反过来说,你没法让一个 Promise 以另一个 Promise 作为成功值,TypeScript 里
await表达式的类型用Awaited<T>递归解包,就是在描述这个行为。
容易答错的地方
- 以为
reject一个 Promise 也会展开 - 只有成功这条路会展开。
reject(somePromise)或者Promise.reject(somePromise)会把这个 Promise 对象本身当作失败原因,catch里拿到的是 Promise,不是它的值。 - 以为返回 Promise 和返回值一样快
- 返回普通值时,下一个
then回调在下一个微任务就能执行。返回原生 Promise 时,要先安排一个任务去调用它的then,再等它的回调通知回来,大约多出两个微任务。单独看无所谓,但在比较打印顺序的题里,这会让「返回 Promise」的那条链落后于其他链。 - 在
then里嵌套then - 写成
.then((u) => { getOrders(u).then((o) => { ... }) })又回到了回调地狱,而且外层链不会等里面的请求,错误也传不出来。正确写法是return getOrders(u),在外面接下一个then。
面试官还会怎么问?
then 回调返回自己所在的那个 Promise 会怎样?
比如 const p = Promise.resolve().then(() => p),外层要等 p 完成,而 p 就是外层自己,永远等不到。规范专门规定了这种情况,p 会以 TypeError 拒绝,Chrome 里的错误信息是 Chaining cycle detected for promise。
new Promise((resolve) => resolve(otherPromise)) 也会展开吗?
会。展开的逻辑在 resolve 函数里,不管是 executor 里的 resolve、then 回调的返回值,还是 Promise.resolve,走的都是同一套判断。区别只是 Promise.resolve 收到原生 Promise 时会直接返回它本身,不再包一层。
await 一个 thenable 会怎样?
await 内部相当于用 Promise.resolve 包一下再等它,所以同样会调用这个对象的 then 方法并等它回调。如果这个对象的 then 永远不调用传进来的函数,await 就会一直卡住。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。