这是一个很酷的问题,需要深入研究。
当你执行以下操作时:
verifier(3,4).then(...)
返回一个新的承诺(promise)对象,需要再次进入事件循环(event loop),在此之前新被拒绝(rejected)的承诺无法运行其后跟的 .catch()
处理程序。这个额外的循环给了下一个序列:
verifier(5,4).then(...)
因为在队列中的先前代码错误处理程序 .catch()
还未执行,所以具有 .then()
处理程序的下一行有机会运行,因为它已经在队列中。而且,项目是按照先进先出的顺序从队列中运行的。
请注意,如果您使用 .then(f1, f2)
形式代替 .then().catch()
,则会在预期时间内运行它,因为没有额外的承诺,因此也没有涉及额外的执行事件:
const verifier = (a, b) =>
new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));
verifier(3, 4)
.then((response) => console.log("response (3,4): ", response),
(error) => console.log("error (3,4): ", error)
);
verifier(5, 4)
.then((response) => console.log("response (5,4): ", response))
.catch((error) => console.log("error (5,4): ", error));
请注意,我还将所有消息标记了,这样您就可以看到它们来自哪个 verifier()
调用,从而更容易阅读输出。
ES6规范关于Promise回调顺序和更详细的解释
ES6规范告诉我们,promise“jobs”(它称为.then()
或.catch()
的回调)按照它们被插入作业队列的时间顺序以FIFO顺序运行。 它没有具体命名FIFO,但它指定新的任务被插入队列末尾,并且任务从队列开头运行。 这实现了FIFO排序。
PerformPromiseThen(执行.then()
回调的操作)将导致EnqueueJob,这是调度解析或拒绝处理程序以实际运行的方式。 EnqueueJob指定挂起的任务添加到作业队列的末尾。 然后,NextJob操作从队列的前面提取项目。 这确保按顺序FIFO处理Promise作业队列中的作业。
因此,在原始问题的示例中,我们得到了为verifier(3,4)
承诺和verifier(5,4)
承诺运行的回调,因为这两个原始承诺都已完成并以它们被运行的顺序插入作业队列。 然后,当解释器回到事件循环时,它首先选择verifier(3,4)
作业。 该承诺被拒绝,并且在verifier(3,4).then(...)
中没有回调。 因此,它拒绝了verifier(3,4).then(...)
返回的承诺,这导致将verifier(3,4).then(...).catch(...)
处理程序插入到jobQueue中。
然后,它回到事件循环,并从作业队列中提取下一个作业,即verifier(5, 4)
作业。 它具有已解决的承诺和解决处理程序,因此调用该处理程序。 这导致显示response (5,4):
输出。
然后,它回到事件循环,并从作业队列中提取下一个作业,即verifier(3,4).then(...).catch(...)
作业,其中运行它,并导致显示error (3,4)
输出。
因为第一个链中的
.catch()
比第二个链中的
.then()
深了一个Promise级别,这导致了你所报告的顺序。而且,由于Promise链是通过作业队列以FIFO顺序从一个级别遍历到下一个级别,而不是同步地进行,这也是造成这种顺序的原因。
关于依赖此级别调度细节的一般建议
一般来说,我尽量编写不依赖于此级别详细时间知识的代码。虽然了解它很有趣,偶尔也很有用,但它是脆弱的代码,因为简单的看似无害的代码更改可能会导致相对时间的变化。所以,如果两个链之间的时间关键,则我宁愿以强制所需的时间方式编写代码,而不是依赖于此级别的详细理解。