JavaScript充满好奇心


96

当我调用此Promise时,输出与函数调用序列不匹配。在.then来之前.catch,即使与承诺.then正在后调用。是什么原因呢?

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
您永远不应依赖于独立的承诺链之间的时间安排。
Bergi

Answers:


136

这是一个很简单的问题。

执行此操作时:

verifier(3,4).then(...)

它返回一个新的Promise,在新拒绝的Promise可以运行.catch()随后的处理程序之前,它需要另一个循环返回事件循环。这个额外的循环给出了下一个序列:

verifier(5,4).then(...)

有机会.then()在前一行之前运行其处理程序,.catch()因为它已经在队列中.catch(),而第一行中的处理程序已进入队列,并且项目以FIFO顺序从队列中运行。


请注意,如果您使用.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 Spec on promise回调排序和更详细的说明

ES6规范告诉我们,诺言“作业”(因为它从a.then()或调用回调.catch())是根据插入作业队列的时间以FIFO顺序运行的。它没有专门命名FIFO,但指定在队列末尾插入新作业,并从队列的开头运行作业。这实现了FIFO排序。

PerformPromiseThen(从中执行回调.then())将导致EnqueueJob,这是安排解析或拒绝处理程序实际运行的方式。EnqueueJob指定将挂起的作业添加到作业队列的后面。然后,NextJob操作从队列的前面拉出项目。这样可确保在Promise作业队列中为作业提供服务时的FIFO顺序。

因此,在原始问题的示例中,我们获得了verifier(3,4)promise的回调,verifier(5,4)并按它们的运行顺序将promise插入到作业队列中,因为这两个原始promise均已完成。然后,当解释器返回到事件循环时,它首先处理verifier(3,4)任务。该承诺被拒绝,并且中没有回调verifier(3,4).then(...)。因此,它所做的就是拒绝verifier(3,4).then(...)返回的诺言,并导致将verifier(3,4).then(...).catch(...)处理程序插入到jobQueue中。

然后,它返回事件循环,并从jobQueue中提取的下一个作业是该verifier(5, 4)作业。它具有一个已解决的Promise和一个已解决的处理程序,因此它将调用该处理程序。这将导致显示response (5,4):输出。

然后,它返回到事件循环,并且从jobQueue中提取的下一个作业是verifier(3,4).then(...).catch(...)运行该作业的作业,这将导致显示error (3,4)输出。

这是因为.catch()第一链中的承诺水平比.then()第二链中的承诺水平更深,这导致了您报告的订购情况。而且,这是因为承诺链是通过作业队列以FIFO顺序从一个级别遍历到下一个级别,而不是同步地遍历。


关于依靠此级别的计划详细信息的一般建议

仅供参考,通常,我尝试编写不依赖于这种详细定时知识水平的代码。尽管它很好奇并且有时有用,但它是易碎的代码,因为对代码的简单看似无害的更改可能会导致相对时间的更改。因此,如果时序在这样的两个链之间至关重要,那么我宁愿以强制时序的方式编写代码,而不希望依赖这种详细的理解。


更具体地说,在promise规范的任何地方都没有记录这种确切的行为,这使它成为实现细节。您可能会在解释器之间(例如,Node.js与Edge和Firefox之间)或不同版本的解释器(例如,Node 12与Node 14)之间获得不同的行为。规范只是说承诺是异步处理的,以避免zalgo代码(恕我直言,BTW误导了它,是因为有人问这样的问题,他们希望依赖于可能的异步代码的时间)
slebetman

@slebetman-是否没有记录将来自不同承诺的承诺回调称为FIFO,具体取决于它们何时插入队列,直到下一个滴答声才运行?似乎FIFO排序就是这里所需要的全部,因为.then()必须返回一个新的承诺,该承诺本身必须在将来的价格变动时异步地解析/拒绝,这就是导致此排序的原因。您是否知道任何不使用竞争性回调的FIFO排序的实现?
jfriend00 '20

3
@slebetman Promises / A +未指定。ES6确实指定了它。(不过,ES11更改了的行为await)。
Bergi

从ES6规范开始排队。 PerformPromiseThen将会导致EnqueueJob解决方案或拒绝处理程序被安排调用的方式。EnqueueJob指定将挂起的作业添加到作业队列的后面。然后,NextJob操作从队列的前面拉出项目。这样可以确保Promise作业队列中的FIFO顺序。
jfriend00

@Bergi ES11中的变化是什么await?一个链接就足够了。谢谢!!
佩德罗

49

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,b1,a2,b2,a3,b3,而不是输出a1,a2,a3,b1,b2,b3-每次返回一个Promise并到达事件循环的结尾队列。因此我们可以看到这场“无极竞赛”。当有一些嵌套的承诺时,情况也是如此。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.