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

时间:2016-04-26 16:18:24

标签: javascript promise es6-promise

我想向自己解释使用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' ...

我的理解是,如果承诺得到解决,那么'然后'函数放在浏览器事件队列中。所以我的期望是1 2 3 4 ...

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

2 个答案:

答案 0 :(得分:68)

<强>评论

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

因此,Promise.resolve()处理程序中的所有.then()承诺都会创建独立于父链运行的新Promise链。您没有确定的行为。这有点像并行启动四个ajax调用。你不知道哪一个会先完成。现在,由于这些Promise.resolve()处理程序中的所有代码都是同步的(因为这不是现实代码),那么您可能会获得一致的行为,但这不是承诺的设计点所以我不会花很多时间来弄清楚哪个只运行同步代码的Promise链将首先完成。在现实世界中,它并不重要,因为如果订单很重要,那么你就不会以这种方式把事情留给机会。

<强>摘要

  1. 在当前执行线程完成后,异步调用所有.then()个处理程序(正如Promises / A +规范所述,当JS引擎返回到&#34;平台代码&#34;)时。即使对于同步解析的承诺(例如Promise.resolve().then(...))也是如此。这样做是为了编程一致性,因此无论是立即解决还是稍后解决,都会异步调用.then()处理程序。这可以防止一些计时错误,并使调用代码更容易看到一致的异步执行。

  2. 如果两者都排队并准备好运行,则没有规范可以确定setTimeout()与预定.then()处理程序的相对顺序。在您的实现中,挂起的.then()处理程序总是在挂起的setTimeout()之前运行,但Promises / A +规范说明这不是确定的。它表示.then()处理程序可以通过多种方式进行调度,其中一些方法可以在挂起的setTimeout()调用之前运行,其中一些可能在挂起的setTimeout()调用之后运行。例如,Promises / A +规范允许.then()处理程序使用setImmediate()进行调度,setTimeout()将在挂起的setTimeout()调用之前运行,或setTimeout()将在挂起.then()之后运行1}}来电。因此,您的代码根本不应该依赖于该订单。

  3. 多个独立的Promise链没有可预测的执行顺序,您不能依赖任何特定的订单。这就像并行发射四个ajax电话一样,你不知道哪一个会先完成。

  4. 如果执行顺序很重要,请不要创建依赖于细微实现细节的竞赛。相反,链接承诺链以强制执行特定的执行顺序。

  5. 您通常不希望在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); 处理程序中创建未从处理程序返回的独立promise链。这通常是一个错误,除非在极少数情况下发生火灾,忘记没有错误处理。

  6. 逐行Analsysis

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

    .then()

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

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

    由于setTimeout(fn, 0)也在这个初始执行线程中,因此它会被运行,因此会安排一个计时器。

    这是同步执行的结束。现在,JS引擎开始运行在事件队列中安排的事物。

    据我所知,无法保证首先是.then().then()处理程序,这些处理程序都计划在此执行线程之后运行。 setTimeout()处理程序被视为&#34;微任务&#34;所以我们在.then()之前首先跑步并不让我感到惊讶。但是,如果您需要特定订单,那么您应该编写保证订单的代码,而不是依赖于此实现细节。

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

    接下来,由于前一个.then()处理程序返回了一个普通值,因此该承诺被视为已解决,因此第4行上定义的.then()处理程序将运行。在这里,您将创建另一个独立的承诺链并引入通常是错误的行为。

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

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

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

    无论如何,在你的情况下,它是一个调度竞赛,你正在运行的引擎决定运行下一行在第5行定义的内部7 "C"处理程序,因此你会看到{{ 1}}在第6行上指定。然后它不返回任何内容,因此该承诺的已解析值变为undefined

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

    回到调度程序中,它决定在第7行上运行.then()处理程序,然后获得8 undefined。承诺存在undefined,因为该链中的前一个.then()处理程序没有返回任何内容,因此其返回值为undefined,因此这是此时的promise链的已解析值

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

    1
    2 "A"
    3 "B"
    7 "C"
    4 "B"
    8 undefined
    

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

    与先前嵌套的Promise.resolve()链一致,计划选择运行第19行上定义的下一个外部.then()处理程序。它运行,你得到输出5 undefined。它又是undefined,因为该链中的前一个.then()处理程序没有返回值,因此promise的已解析值为undefined

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

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

    此时,只有一个.then()处理程序计划运行,因此它运行在第15行上定义的处理程序,然后您将获得输出10 undefined。< / p>

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

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

    如果有人试图准确预测这种情况会发生的顺序,那么就会有两个主要问题。

    1. 待处理的.then()处理程序如何优先处理,而setTimeout()调用也处于待处理阶段。

    2. 承诺引擎如何决定优先处理等待运行的多个.then()处理程序。根据此代码的结果,它不是FIFO。

    3. 对于第一个问题,我不知道这是每个规范还是仅仅是promise引擎/ JS引擎中的实现选择,但您报告的实现似乎优先处理所有待处理的.then()任何setTimeout()来电之前的处理程序。您的情况有点奇怪,因为除了指定.then()处理程序之外,您没有实际的异步API调用。如果你有任何异步操作实际上在这个promise链的开头实际执行,那么你的setTimeout()将在真正的异步操作的.then()处理程序之前执行,因为真正的异步操作需要实际时间来执行。所以,这是一个人为的例子,并不是真正代码的通常设计案例。

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

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

      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);
      

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

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

      并且,由于承诺全部链接在一起,承诺顺序全部由代码定义。作为实现细节留下的唯一内容是setTimeout()的时间,如在您的示例中,在所有待处理的.then()处理程序之后,它是最后的。

      修改

      在检查Promises/A+ specification后,我们发现:

        

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

           

      ...

           

      3.1此处“平台代码”表示引擎,环境和承诺实现代码。在实践中,这个要求确保了这一点   在事件发生后,onFulfilled和onRejected异步执行   循环转入然后调用,并使用新堆栈。这可以   使用“宏任务”机制(如setTimeout或)实现   setImmediate,或者使用“微任务”机制   MutationObserver或process.nextTick。自承诺实施以来   被认为是平台代码,它本身可能包含任务调度   队列或“trampoline”,其中调用处理程序。

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

      我在ES6规范中找不到与process.nextTick()相关的宏任务,微任务或承诺.then()处理程序的时间信息。这可能并不奇怪,因为setTimeout()本身不是ES6规范的一部分(它是主机环境功能,而不是语言功能)。

      我还没有找到任何规范来支持这一点,但是这个问题的答案Difference between microtask and macrotask within an event loop context解释了在具有宏任务和微任务的浏览器中它们的运作方式。

      仅供参考,如果您想了解有关微任务和宏任务的更多信息,请参阅以下主题:Tasks, microtasks, queues and schedules

答案 1 :(得分:1)

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

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

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