先记住这个答案
Promise.resolve(thenable) 不会把这个对象原样还给你。它会新建一个 Promise,然后在下一个微任务里调用 thenable.then(resolve, reject),把新 Promise 的 resolve 和 reject 交给它。then 里调了 resolve(42),新 Promise 就以 42 兑现;调了 reject,就被拒绝。只有传进来的本身就是原生 Promise 实例时,Promise.resolve 才会直接返回同一个对象。
- thenable 就是带
then方法的对象或函数 - 返回的是新 Promise,结果由
then里调用的回调决定 then在下一个微任务才被调用,不是同步调用- 只有原生 Promise 实例才会被原样返回
先看一个 thenable 被展开的过程
thenable 指的是任何带 then 方法的对象或函数。它不需要是 Promise,只要长得像就行。比如下面这个普通对象:
const thenable = {
then(resolve, reject) {
console.log('then 被调用')
resolve(42)
}
}
const p = Promise.resolve(thenable)
console.log('同步代码结束', p === thenable) // p 是新建的 Promise
p.then((v) => console.log('结果', v))查看输出与解释
同步代码结束 false
then 被调用
结果 42先打印的是「同步代码结束」,说明 then 不是在 Promise.resolve 调用时同步执行的,而是被放进了微任务队列。等同步代码跑完,引擎才去调用 thenable.then,这时 resolve(42) 让 p 变成 fulfilled,p.then 的回调再进下一个微任务,打印出 42。
可以把 Promise.resolve 想成一个转接头:外面插进来的是什么牌子的插头都行,只要有 then 这个接口,它就能转成标准 Promise。
什么时候会原样返回
原样返回的条件很窄:参数必须是 Promise 实例,而且它的 constructor 正好是调用方的 Promise。子类的实例、自己手写的 Promise 都不算。
const native = Promise.resolve(1)
console.log(Promise.resolve(native) === native) // 原样返回
console.log(new Promise((r) => r(native)) === native) // 构造函数总是新建
class MyPromise extends Promise {}
const mine = MyPromise.resolve(1)
console.log(Promise.resolve(mine) === mine) // constructor 是 MyPromise,不是 Promise查看输出与解释
true
false
falsenew Promise((r) => r(native)) 永远得到新对象,而且它会把 native 当 thenable 走一遍上面的流程,比 Promise.resolve(native) 多花两个微任务才兑现。所以把已有的 Promise 统一成 Promise 时,直接用 Promise.resolve 更省。
- 为什么要兼容 thenable
- ES6 之前社区里已经有 Q、Bluebird、jQuery Deferred 等一堆 Promise 实现。Promises/A+ 规范约定大家都认
then方法,这样不同库的 Promise 能互相串起来。原生 Promise 延续了这个约定,await一个 thenable 也是同样的处理方式。
then 写得不规范时会怎样
thenable 是外部代码,可能乱调回调。Promise 的做法是只认第一次,后面的调用和异常一律忽略。
resolve 加抛错JavaScriptconst bad = {
then(resolve, reject) {
resolve('第一次')
resolve('第二次') // 被忽略
throw new Error('已经 resolve 过,抛错也被忽略')
}
}
Promise.resolve(bad).then(
(v) => console.log('fulfilled', v),
(e) => console.log('rejected', e.message)
)查看输出与解释
fulfilled 第一次反过来,如果 then 在调用任何回调之前就抛错,新 Promise 会以这个错误被拒绝。如果 then 什么都不调,新 Promise 就永远 pending,这是最难排查的一种,因为不会有任何报错。
容易答错的地方
- 说
Promise.resolve会直接返回传入的对象 - 这只对原生 Promise 实例成立。thenable、子类实例、普通值都会得到一个新 Promise。面试里把 thenable 和原生 Promise 混为一谈,面试官基本会接着追问区别。
- 以为
then是同步调用的 then方法被放进微任务里调用,所以Promise.resolve(thenable)之后紧跟的同步代码会先执行。但读取then这个属性是同步的,如果then是个会抛错的 getter,错误会在调用时就被捕获,返回的 Promise 直接变成 rejected。- 以为只有对象才算 thenable
- 函数也是对象,一个挂了
then方法的函数同样会被当成 thenable。反过来,then属性不是函数的对象只会被当普通值,直接兑现为这个对象本身。
面试官还会怎么问?
await 一个 thenable 会发生什么?
和 Promise.resolve 一样。await 内部先把值转成 Promise,遇到 thenable 就调用它的 then,拿到 resolve 的值作为 await 表达式的结果。这也是为什么有些 ORM 的查询构造器没有真的返回 Promise,却可以直接 await。
为什么 resolve 一个 Promise 会多出两个微任务?
在 new Promise 的 executor 里调用 resolve(p) 时,p 被当成 thenable。第一个微任务去调用 p.then,这时 p 已经兑现,它的回调再排一个微任务,才轮到外层 Promise 兑现。这就是 async 函数里 return 一个 Promise 会比 return 普通值晚两拍的原因。
怎么判断一个值是不是 thenable?
规范的判断方式是:值是对象或函数,并且它的 then 属性是函数。写成代码就是 value !== null && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function'。不要用 instanceof Promise 判断,那样会漏掉其它库的 Promise。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。