Promise.race polyfill中then双参与then().catch()行为差异疑问
问题现象
编写Promise.race() polyfill时,最初实现代码如下:
const promise = new Promise((resolve, reject) => { resolve('promise resolved'); }); const promise2 = Promise.resolve('promise2 resolved'); function raceCopy(promises) { return new Promise(function (resolve, reject) { for (var index = 0; index < promises.length; index++) { Promise.resolve(promises[index]) .then(resolve) .catch(reject); } }); } raceCopy([Promise.reject('copy rejected'), promise, promise2]) .then((response) => console.log(response)) .catch((error) => console.error(error));
预期控制台输出错误信息copy rejected,实际输出了promise resolved。
对照验证结果如下:
- 使用原生
Promise.race()执行相同入参,可正确输出copy rejected:Promise.race([Promise.reject('copy rejected'), promise, promise2]) .then((response) => console.log(response)) .catch((error) => console.error(error)); - 将链式调用的
.then(resolve).catch(reject)替换为.then(resolve, reject)后,polyfill运行结果正确:function raceCopy(promises) { return new Promise(function (resolve, reject) { for (var index = 0; index < promises.length; index++) { Promise.resolve(promises[index]).then(resolve, reject); } }); }
两种写法看似逻辑等价,运行结果却完全不同。
核心原理
两种写法完全不等价,差异来自Promise链式调用的两个核心规则:
- 每一次
.then()/.catch()调用都会返回一个全新的Promise实例,会产生额外的微任务延迟 - Promise状态一旦从pending变更为fulfilled/rejected,后续所有对该Promise的resolve/reject调用都会被直接忽略
错误写法的执行流程
逐行拆解.then(resolve).catch(reject)写法的微任务入队、执行顺序:
循环按索引从0到2遍历入参数组,所有回调注册逻辑是同步执行的:
- 处理索引0的rejected状态Promise(值为
copy rejected):- 调用
.then(resolve)时,因为当前Promise是rejected,且没有传入onRejected处理函数,JS引擎会生成默认的透传rejection的回调,将这个默认回调加入微任务队列,此时.then()返回的新Promise处于pending状态 - 后续的
.catch(reject)只是给这个pending状态的新Promise注册reject回调,并不会立刻把外层的reject方法加入微任务队列,需要等前一个.then()返回的Promise敲定状态后才会触发
- 调用
- 处理索引1的resolved状态Promise(值为
promise resolved):- 调用
.then(resolve)时,当前Promise是resolved状态,直接把外层resolve回调加入微任务队列 - 后续的
.catch(reject)仅注册错误回调,暂不触发
- 调用
- 处理索引2的resolved状态Promise(值为
promise2 resolved):- 同索引1的逻辑,把对应的外层resolve回调加入微任务队列,排在索引1的resolve回调之后
同步代码执行完毕,开始按顺序清空微任务队列:
- 第一个微任务:执行索引0Promise的默认透传回调,将.then()返回的新Promise置为rejected状态,此时才把外层reject回调推到微任务队列末尾。此时队列顺序为:
[resolve('promise resolved'), resolve('promise2 resolved'), reject('copy rejected')] - 第二个微任务:执行外层resolve('promise resolved'),race返回的Promise从pending变为fulfilled,后续所有resolve/reject调用全部失效
- 后续两个微任务执行时,因为外层Promise已经敲定状态,调用resolve、reject都不会产生任何效果,最终输出
promise resolved
正确写法的执行流程
.then(resolve, reject)是直接在Promise.resolve(promises[index])返回的Promise上同时注册onFulfilled、onRejected两个回调,没有中间的新Promise层,不会产生额外微任务延迟:
- 处理索引0的rejected Promise时,直接把外层reject回调加入微任务队列,排在队列最前端
- 清空微任务时,第一个执行的就是reject('copy rejected'),直接把race返回的Promise置为rejected状态,后续的resolve回调全部被忽略。
这和原生Promise.race的实现逻辑一致:所有传入Promise的状态敲定回调都是直接注册,没有中间层延迟,因此最先敲定的Promise(无论状态是fulfilled还是rejected)会直接决定返回Promise的最终状态,结果符合预期。
额外补充
两种写法的另一个常被忽略的差异是错误捕获范围不同:
.then(resolve, reject)中的reject回调,只能捕获当前Promise的rejection,无法捕获resolve回调执行过程中抛出的错误.then(resolve).catch(reject)中的catch回调,不仅能捕获原始Promise的rejection,还能捕获resolve回调执行时抛出的错误,只是这个特性在Promise.race的polyfill场景下,因为额外的微任务延迟导致了顺序错误。
内容的提问来源于stack exchange,提问作者Himanshu singh
相关产品推荐
相关产品推荐

