JavaScript Promises好奇心

100
当我调用这个Promise时,输出结果与函数调用的顺序不匹配。即使带有.then的Promise是在后面调用的,.then也会先执行,而.catch却后执行。这是什么原因?

const verifier = (a, b) =>
  new Promise((resolve, reject) => (a > b ? resolve(true) : reject(false)));

verifier(3, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

verifier(5, 4)
  .then((response) => console.log("response: ", response))
  .catch((error) => console.log("error: ", error));

输出

node promises.js
response: true
error: false

34
你永远不应该依赖于独立的 Promise 链之间的时间关系。 - Bergi
2个回答

138

这是一个很酷的问题,需要深入研究。

当你执行以下操作时:

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顺序从一个级别遍历到下一个级别,而不是同步地进行,这也是造成这种顺序的原因。

关于依赖此级别调度细节的一般建议

一般来说,我尽量编写不依赖于此级别详细时间知识的代码。虽然了解它很有趣,偶尔也很有用,但它是脆弱的代码,因为简单的看似无害的代码更改可能会导致相对时间的变化。所以,如果两个链之间的时间关键,则我宁愿以强制所需的时间方式编写代码,而不是依赖于此级别的详细理解。


更具体地说,这种确切的行为在承诺规范的任何地方都没有记录,这使得它成为一项实现细节。您可能会在解释器之间(例如Node.js vs Edge vs Firefox)或解释器版本之间(例如Node 12 vs Node 14)获得不同的行为。规范仅表示承诺是异步处理的,以避免zalgo代码(我认为这是错误的,因为它是由那些想要依赖潜在异步代码时间的人提出的问题所驱动的)。 - slebetman
@slebetman - 这不是有文档说明来自不同承诺的承诺回调按照它们插入队列的顺序进行FIFO调用,并且不能在下一个刻度之前运行吗?似乎这里所需的只是FIFO排序,因为.then()必须返回一个新的承诺,该承诺本身必须在未来的刻度上异步解决/拒绝,这就导致了这种排序。您是否知道任何不使用竞争回调的FIFO排序的实现? - jfriend00
3
Promises/A+ 没有规定这个。ES6 规定了它。(尽管 ES11 改变了 await 的行为)。 - Bergi
从ES6规范的排队顺序中可以看出,PerformPromiseThen将导致EnqueueJob,这是调用解决或拒绝处理程序的方式。EnqueueJob指定待处理的作业被添加到作业队列的末尾。然后,NextJob操作从队列的前面拉取该项。这确保了Promise作业队列中的FIFO顺序。 - jfriend00
@Bergi,ES11中await的变化是什么?一个链接就足够了。谢谢! - Pedro A
@PedroA https://dev59.com/O7voa4cB1Zd3GeqPzzxr, https://stackoverflow.com/questions/61234256/different-promise-execution-on-browsers-what-happened - Bergi

51

Promise.resolve()
  .then(() => console.log('a1'))
  .then(() => console.log('a2'))
  .then(() => console.log('a3'))
Promise.resolve()
  .then(() => console.log('b1'))
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))

不会输出a1,a2,a3,b1,b2,b3,因为每个then返回一个promise并进入事件循环队列的末尾,所以你将看到a1,b1,a2,b2,a3,b3。因此,我们可以看到这种“promise race”。当存在一些嵌套的promise时也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接