在javascript承诺中执行的顺序是什么

问题描述 投票:27回答:2

我想向自己解释下面使用javascript promises的代码片段的执行顺序。

Promise.resolve('A')
  .then(function(a){console.log(2, a); return 'B';})
  .then(function(a){
     Promise.resolve('C')
       .then(function(a){console.log(7, a);})
       .then(function(a){console.log(8, a);});
     console.log(3, a);
     return a;})
  .then(function(a){
     Promise.resolve('D')
       .then(function(a){console.log(9, a);})
       .then(function(a){console.log(10, a);});
     console.log(4, a);})
  .then(function(a){
     console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);

结果是:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

我很好奇执行顺序1 2 3 7 ...而不是值'A','B'......

我的理解是,如果一个promise得到解决,'then'函数将被放入浏览器事件队列中。所以我的期望是1 2 3 4 ......


@ jfriend00谢谢,非常感谢您的详细解释!这真是一项巨大的工作!

javascript promise es6-promise
2个回答
66
投票

评论

首先,在.then()处理程序中运行promises并且不从.then()回调中返回这些promises会创建一个全新的未附加的promise序列,它不会以任何方式与父promises同步。通常,这是一个错误,事实上,一些承诺引擎实际上会在您这样做时发出警告,因为它几乎不是所希望的行为。人们想要做的唯一一次就是当你做某种火灾并忘记操作时你不关心错误而你不关心与世界其他地方的同步。

所以,你在Promise.resolve()处理程序中所有的.then()承诺创建了独立于父链的新Promise链。您没有确定的行为。这有点像并行启动四个ajax调用。你不知道哪一个会先完成。现在,因为那些Promise.resolve()处理程序中的所有代码都是同步的(因为这不是真正的世界代码),那么你可能会得到一致的行为,但这不是承诺的设计点所以我不会花太多时间试图找出只运行同步代码的Promise链将首先完成。在现实世界中,这并不重要,因为如果秩序很重要,那么你就不会以这种方式把事情搞得一团糟。

摘要

  1. 在当前执行线程完成后,所有.then()处理程序都是异步调用的(正如Promises / A +规范所说,当JS引擎返回“平台代码”时)。即使对于同步解析的承诺(例如Promise.resolve().then(...))也是如此。这是为了编程一致性,因此无论是立即解决还是稍后解决,都会异步调用.then()处理程序。这可以防止一些计时错误,并使调用代码更容易看到一致的异步执行。
  2. 没有规范可以确定setTimeout()与已安排的.then()处理程序的相对顺序(如果它们都已排队并准备运行)。在您的实现中,挂起的.then()处理程序总是在挂起的setTimeout()之前运行,但Promises / A +规范说明这不是确定的。它说.then()处理程序可以安排多种方式,其中一些在挂起的setTimeout()调用之前运行,其中一些可能在挂起的setTimeout()调用之后运行。例如,Promises / A +规范允许.then()处理程序使用setImmediate()进行调度,setTimeout()将在挂起的setTimeout()调用之前运行,或者使用setTimeout()运行,该.then()将在挂起的1 Promise.resolve('A').then(function (a) { 2 console.log(2, a); 3 return 'B'; 4 }).then(function (a) { 5 Promise.resolve('C').then(function (a) { 6 console.log(7, a); 7 }).then(function (a) { 8 console.log(8, a); 9 }); 10 console.log(3, a); 11 return a; 12 }).then(function (a) { 13 Promise.resolve('D').then(function (a) { 14 console.log(9, a); 15 }).then(function (a) { 16 console.log(10, a); 17 }); 18 console.log(4, a); 19 }).then(function (a) { 20 console.log(5, a); 21 }); 22 23 console.log(1); 24 25 setTimeout(function () { 26 console.log(6) 27 }, 0); 调用之后运行。因此,您的代码根本不应该依赖于该顺序。
  3. 多个独立的Promise链没有可预测的执行顺序,您不能依赖任何特定的顺序。这就像并行发射四个ajax调用,你不知道哪一个会先完成。
  4. 如果执行顺序很重要,请不要创建依赖于详细实现细节的竞赛。相反,链接承诺链以强制执行特定的执行顺序。
  5. 您通常不希望在.then()处理程序中创建未从处理程序返回的独立promise链。这通常是一个错误,除非在极少数情况下发生火灾,忘记没有错误处理。

逐行分析

所以,这是对你的代码的分析。我添加了行号并清理了缩进,以便更容易讨论:

Promise.resolve()

第1行启动一个承诺链并附加一个.then()处理程序。由于.then()立即解析,Promise库将安排第一个console.log(1)处理程序在此Javascript线程完成后运行。在Promises / A +兼容的promise库中,所有.then()处理程序在当前执行线程完成后以及JS返回事件循环时异步调用。这意味着此线程中的任何其他同步代码(例如setTimeout())将在下一个运行,这就是您所看到的。

顶级(第4,12,19行)的所有其他setTimeout(fn, 0)处理程序在第一个之后链接,并且仅在第一个转向之后运行。它们在这一点基本上排队。

由于.then()也在这个初始执行线程中,因此它被运行,因此计划了一个计时器。

那是同步执行的结束。现在,JS引擎开始运行在事件队列中调度的内容。

据我所知,无法保证首先是.then()setTimeout()处理程序,它们都被安排在这个执行线程之后运行。 .then()处理程序被认为是“微任务”,因此我们在2 "A"之前首先运行并不会让我感到惊讶。但是,如果您需要特定订单,那么您应该编写保证订单的代码,而不是依赖于此实现细节。

无论如何,第1行定义的console.log(2, a)处理程序接下来运行。因此,您可以看到来自.then()的输出.then()

接下来,由于前面的.then()处理程序返回了一个普通值,因此该承诺被认为已解析,因此第4行定义的console.log(3, a)处理程序运行。这是您创建另一个独立的承诺链并引入通常是错误的行为的地方。

第5行,创建一个新的Promise链。它解析了初始承诺,然后调度两个.then()处理程序在当前执行线程完成时运行。在当前执行的线程是第10行的return Promise.resolve.then(...) ,这就是为什么你看到下一个。然后,这个执行线程结束,然后返回调度程序,看看接下来要运行什么。

我们现在有几个.then()处理程序在队列中等待下一个运行。我们刚刚在第5行安排了一个,第12行的更高级链中有下一个。如果你在第5行完成了这个:

.then()

然后你会将这些承诺联系在一起,它们将按顺序协调。但是,通过不返回承诺值,您开始了一个全新的承诺链,而不是与外部的更高级别承诺协调。在您的特定情况下,promise调度程序决定接下来运行更深层嵌套的7 "C"处理程序。我真的不知道这是通过规范,按惯例还是只是一个承诺引擎与另一个承诺引擎的实现细节。我会说,如果订单对你很重要,那么你应该通过按特定顺序链接承诺来强制执行订单,而不是依靠谁赢得竞争首先运行。

无论如何,在你的情况下,它是一个调度竞赛,你正在运行的引擎决定运行在第5行定义的内部undefined处理程序,因此你看到第6行指定的.then()。它然后什么也没有返回,所以解析了这个承诺变成.then()

回到调度程序,它在第12行运行.then()处理程序。这又是.then()处理程序和第7行之间的竞争,它也在等待运行。我不知道为什么它在这里选择一个而不是说它可能是不确定的或因每个承诺引擎而异,因为代码没有指定顺序。无论如何,第12行中的4 "B"处理程序开始运行。这再次创建了一个新的独立或不同步的承诺链线。它再次调度.then()处理程序,然后从.then()处理程序中的同步代码中获取8 undefined。所有同步代码都在该处理程序中完成,现在,它将返回到调度程序以执行下一个任务。

回到调度程序,它决定在第7行运行undefined处理程序,你得到.then()。承诺存在undefined,因为该链中的先前1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 处理程序没有返回任何内容,因此其返回值为.then(),因此这是此时承诺链的已解析值。

此时,到目前为止的输出是:

9 "D"

同样,所有同步代码都已完成,因此它再次返回调度程序,并决定运行第13行定义的Promise.resolve()处理程序。运行并获得输出.then(),然后再次返回调度程序。

与先前嵌套的5 undefined链一致,该计划选择运行在第19行定义的下一个外部undefined处理程序。它运行并获得输出.then()。它再次是undefined,因为该链中的前一个1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 9 "D" 5 undefined 处理程序没有返回值,因此promise的解析值是.then()

至此,到目前为止的输出是:

10 undefined

此时,只有一个setTimeout()处理程序计划运行,因此它运行第15行定义的处理程序,然后接下来输出1 2 "A" 3 "B" 7 "C" 4 "B" 8 undefined 9 "D" 5 undefined 10 undefined 6

然后,最后,.then()开始运行,最终输出为:

setTimeout()

如果一个人试图准确地预测这将会发生的顺序,那么将会有两个主要问题。

  1. 挂起的.then()处理程序如何优先处理,而.then()调用也处于待决状态。
  2. promise引擎如何决定优先处理等待运行的多个setTimeout()处理程序。根据此代码的结果,它不是FIFO。

对于第一个问题,我不知道这是每个规范还是仅仅是promise引擎/ JS引擎中的实现选择,但是您报告的实现似乎在任何.then()调用之前优先处理所有挂起的setTimeout()处理程序。您的情况有点奇怪,因为除了指定.then()处理程序之外,您没有实际的异步API调用。如果你有任何异步操作实际上在这个promise链的开头实际执行,那么你的.then()将在真正的异步操作的Promise.resolve('A').then(function (a) { console.log(2, a); return 'B'; }).then(function (a) { var p = Promise.resolve('C').then(function (a) { console.log(7, a); }).then(function (a) { console.log(8, a); }); console.log(3, a); // return this promise to chain to the parent promise return p; }).then(function (a) { var p = Promise.resolve('D').then(function (a) { console.log(9, a); }).then(function (a) { console.log(10, a); }); console.log(4, a); // return this promise to chain to the parent promise return p; }).then(function (a) { console.log(5, a); }); console.log(1); setTimeout(function () { console.log(6) }, 0); 处理程序之前执行,因为真正的异步操作需要实际执行时间。所以,这是一个人为的例子,并不是真正代码的通常设计案例。

对于第二个问题,我已经看到一些讨论,讨论如何优先处理不同嵌套级别的1 2 "A" 3 "B" 7 "C" 8 undefined 4 undefined 9 "D" 10 undefined 5 undefined 6 处理程序。我不知道该讨论是否曾在规范中得到解决。我更喜欢以一种细节级别对我来说无关紧要的方式进行编码。如果我关心我的异步操作的顺序,那么我链接我的promise链来控制顺序,这个级别的实现细节不会以任何方式影响我。如果我不关心订单,那么我不关心订单,所以实施细节的水平不会影响我。即使这是在某种规范中,看起来这种细节类型不应该被许多不同的实现(不同的浏览器,不同的承诺引擎)所信任,除非你已经在你要运行的任何地方测试过它。因此,当您有不同步的承诺链时,我建议不要依赖特定的执行顺序。


您可以通过链接所有您的承诺链来使订单100%确定(返回内部承诺,以便它们链接到父链):

setTimeout()

这会在Chrome中提供以下输出:

.then()

并且,由于承诺全部链接在一起,承诺顺序全部由代码定义。作为一个实现细节留下的唯一的东西是Promises/A+ specification的时间,在你的例子中,在所有挂起的.then()处理程序之后,它是最后的。

编辑:

在检查setTimeout()后,我们发现:

2.2.4 onFulfilled或onRejected在执行上下文堆栈仅包含平台代码之前不得调用。 [3.1]。

....

3.1这里“平台代码”表示引擎,环境和承诺实现代码。实际上,这个要求确保onFulfilled和onRejected异步执行,然后调用事件循环,然后调用新堆栈。这可以使用诸如setTimeout或setImmediate之类的“宏任务”机制,或者使用诸如MutationObserver或process.nextTick之类的“微任务”机制来实现。由于promise实现被认为是平台代码,因此它本身可能包含一个任务调度队列或“trampoline”,其中调用处理程序。

这表示process.nextTick()处理程序必须在调用堆栈返回平台代码后异步执行,但完全将其完全留给实现,无论是使用像.then()这样的宏任务还是像setTimeout()这样的微任务完成的。因此,根据此规范,它不是确定的,不应该依赖。

在ES6规范中,我找不到与setTimeout()相关的宏任务,微任务或承诺Difference between microtask and macrotask within an event loop context处理程序的时间信息。这可能并不奇怪,因为Tasks, microtasks, queues and schedules本身不是ES6规范的一部分(它是主机环境功能,而不是语言功能)。

我没有找到任何规范来支持这一点,但是这个问题的答案qazxswpoi解释了在具有宏任务和微任务的浏览器中事物是如何工作的。

仅供参考,如果您想了解有关微任务和宏任务的更多信息,请参阅以下有关该主题的有趣参考文章:qazxswpoi。


1
投票

浏览器的JavaScript引擎有一个叫做“事件循环”的东西。一次只运行一个JavaScript代码线程。单击按钮或AJAX请求或其他任何异步完成时,会在事件循环中放置一个新事件。浏览器一次执行一个这些事件。

你在这里看到的是你运行异步执行的代码。异步代码完成后,它会向事件循环添加适当的事件。添加事件的顺序取决于每个异步操作完成所需的时间。

这意味着如果您使用的是像AJAX这样的东西,您无法控制请求将在何种顺序完成,那么您的承诺每次都可以以不同的顺序执行。

© www.soinside.com 2019 - 2024. All rights reserved.