先记住这个答案
Promise.resolve(thenable) 会创建一个新 Promise,并在微任务中调用 thenable 的 then 方法,传入内部准备好的 resolve 和 reject 回调。随后该 Promise 的状态由 thenable 的行为决定:若 thenable 调用 onFulfilled,则外层 Promise 以传入值完成;若调用 onRejected 或以异常结束,则外层拒绝。如果 onFulfilled 回调接收到的参数是另一个 thenable,则会继续递归展开,直到得到非 thenable 值或拒绝。
thenable会被调用then方法并异步展开- 原生 Promise 直接返回原引用,不走
then - 展开是递归过程,直到非 thenable 值
展开 thenable 的内部步骤
当 Promise.resolve(value) 遇到 value 是对象或函数且具有 then 方法时,它不会直接完成,而是创建一个新的 Promise,并在当前任务结束后(微任务时机)调用 value.then,传入两个内部函数:onFulfilled 和 onRejected。这与原生 Promise 不同,因为原生 Promise 实例被特殊处理,直接返回原引用,不会重新调用其 then 方法。
如果 thenable 的 then 方法调用 onFulfilled,则外层 Promise 以传入的参数作为完成值。若该参数又是一个 thenable,则递归重复上述过程,直到值不再具有 then 方法。onRejected 被调用或 then 方法抛错时,外层立即拒绝。若 thenable 同时调用两个回调,以先发生的为准,后调用被忽略。
适配一个遗留回调式库
假设有一个旧函数 readFile(path, callback),它不返回 Promise,而是回调成功值或错误。为了在新代码中与 Promise 链互相操作,可以手工封装,但更稳妥的是利用 thenable:传入一个对象 { then(resolve, reject) { readFile(path, (err, data) => err ? reject(err) : resolve(data)); } }。若直接 Promise.resolve(thenable),内部会异步执行 then,从而把结果无缝转换为 Promise。
这个场景中,thenable 的 then 方法在微任务里调用,避免了回调地狱。关键在于,被封装对象不需要真正实现 Promise 的完整规范,只要遵守“调用两个回调之一一次”的约定即可。这与原生 Promise 不同,因为如果你的库已经返回了一个 Promise,直接 Promise.resolve(promise) 会原样返回,不会再次包装。使用 thenable 可以把任意符合类似行为的接口统一成 Promise。
展开的适用边界与陷阱
展开并非万能,若 thenable 的 then 方法根本不调用任何回调且不抛错,外层 Promise 会永远处于 pending 状态,这会导致调用 .then 后所有后续逻辑无法触发。另外,如果 thenable 的 then 方法在调用 onFulfilled 之后又抛出异常,该异常会被忽略,因为 Promise 的状态已经确定。MDN 示例明确展示了“先调用再抛出”并无影响。
另一个边界是 thenable 自我引用:then 方法调用 onFulfilled 时传入对象自身,这会引发无限递归展开,因为每次展开都会尝试继续解析新的 thenable,最终栈溢出。因此,需要避免设计这种对象。如果必须处理可能自引用的数据,应在传入前检查引用或添加深度控制。实际编码中外层 Promise 的状态完全取决于 thenable 的行为,所以对第三方实现的不可靠代码要格外小心,必要时用超时(如 Promise.race)保护挂起风险。
容易答错的地方
- 把 thenable 当成值直接完成
- 错误认识:
Promise.resolve({ then: () => {} })会直接成功并返回该对象。正确行为是,该 thenable 会被调用then方法,由于没调用 resolve,Promise 永远 pending。只有不带then方法的普通对象才会被直接完成。 - 认为原生 Promise 也会被重新包裹
- 错误认识:
Promise.resolve(existingPromise)会创建新 Promise 并重新执行其then。实际上,如果传入的是原生 Promise 实例且其构造函数一致,Promise.resolve直接返回该实例,不产生新 Promise(original === cast为 true)。
面试官还会怎么问?
如果 thenable 的 then 方法抛出了同步错误,Promise.resolve 会拒绝吗?
会拒绝。Promise.resolve 内部创建 Promise 后,在微任务中调用 thenable 的 then,其抛出的异常会触发内部 reject,导致外层 Promise 变为拒绝状态。但如果传给 Promise.resolve 的参数表达式本身在求值时抛错(例如 someFn() 抛出异常),则该错误发生在 Promise.resolve 调用之前,不会被捕获,此时需要使用 Promise.try 或先 try 包裹。
Promise.resolve 与 new Promise(resolve => resolve(thenable)) 等价吗?
对非原生 Promise 的 thenable 而言基本等价,但 Promise.resolve 有特殊优化:直接返回原生 Promise 实例。而 new Promise 总会创建新 Promise,且其解析逻辑也委托给内部 resolve,两者最终展开行为一致,但 Promise.resolve 更简洁且符合惯例。
嵌套多层 thenable 时,Promise 最终用什么值完成?
递归展开直到遇到非 thenable 值。例如值完成到另一个 thenable,再完成到数字,最终 Promise 以该数字完成。但如果其中某层调用 onRejected,则立即拒绝,后续层不再处理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。