javascript - JavaScript promises中的执行顺序是什么?

标签 javascript promise es6-promise

我想了解以下使用JavaScript promise 的代码段的执行顺序。

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 ...
为什么没有1 2 3 4 ...记录的顺序?

最佳答案

评论

首先,在.then()处理程序中运行promise,而不从.then()回调中返回那些 promise ,会创建一个全新的未附加 promise 序列,该序列不会以任何方式与父 promise 同步。通常,这是一个错误,实际上,某些 promise 引擎实际上会在您执行此操作时发出警告,因为这几乎绝不是所希望的行为。唯一想做的就是,当您执行某种类型的“开火”而忘记操作时,您不在乎错误,也不在乎与世界其他地方的同步。

因此,您在Promise.resolve()处理程序中的所有.then() Promise都会创建独立于父链运行的新Promise链。您没有确定的行为。这有点像并行启动四个ajax调用。您不知道哪个会先完成。现在,由于这些Promise.resolve()处理程序中的所有代码都恰好是同步的(因为这不是真实世界的代码),因此您可能会得到一致的行为,但这不是Promise的设计重点,因此我不会花费太多时间试图找出仅运行同步代码的Promise链将首先完成。在现实世界中,这无关紧要,因为如果顺序很重要,那么您就不会有这种机会。

摘要

  • 当前执行线程完成后(如Promises/A +规范所述,当JS引擎返回“平台代码”时),将异步调用所有.then()处理程序。即使对于同步解决的 promise (例如Promise.resolve().then(...))也是如此。这样做是为了保持编程的一致性,因此无论是否立即解决 promise ,始终都会异步调用.then()处理程序。这样可以防止出现一些计时错误,并使调用代码更容易看到一致的异步执行。
  • 没有规范可以确定setTimeout()与计划的.then()处理程序的相对顺序(如果两者都已排队并且可以运行)。在您的实现中,待处理的.then()处理程序总是在待处理的setTimeout()之前运行,但是Promises/A +规范说明并不确定。它说.then()处理程序可以通过多种方式进行调度,其中某些方法将在未决setTimeout()调用之前运行,而某些可能在未决setTimeout()调用之后运行。例如,Promises/A +规范允许使用在待处理的.then()调用之前运行的setImmediate()或在待处理的setTimeout()调用之后运行的setTimeout()来调度setTimeout()处理程序。因此,您的代码完全不应该依赖于该顺序。
  • 多个独立的Promise链没有可预测的执行顺序,因此您不能依赖任何特定顺序。就像并行触发四个ajax调用一样,您不知道哪个将首先完成。
  • 如果执行顺序很重要,请不要创建依赖于详细实现细节的竞赛。相反,链接 promise 链可以强制执行特定的执行顺序。
  • 通常,您不想在.then()处理程序中创建未从处理程序返回的独立 promise 链。这通常是一个错误,除非在极少数情况下会发生火灾,并且在没有错误处理的情况下忘记了。

  • 逐行肛门分析

    因此,这是您的代码分析。我添加了行号并清理了缩进,以便于讨论:
    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);
    

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

    所有其他在顶层的.then()处理程序(第4、12、19行)都在第一个处理程序之后链接,并且仅在第一个处理程序轮到之后运行。他们基本上在这一点上排队。

    由于setTimeout()也在此初始执行线程中,因此它会运行并因此安排了计时器。

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

    据我所知,不能保证首先出现一个setTimeout(fn, 0).then()处理程序,它们都计划在此执行线程之后立即运行。 .then()处理程序被视为“微任务”,因此它们在setTimeout()之前首先运行并不奇怪。但是,如果需要特定的命令,则应编写保证命令的代码,而不要依赖于此实现细节。

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

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

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

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

    那么您将把这些 promise 联系在一起,并且它们将按顺序进行协调。但是,通过不返回 promise 值,您启动了一个与外部更高级别的 promise 不协调的全新的 promise 链。在您的特定情况下,promise调度程序决定接下来运行更深层嵌套的.then()处理程序。老实说,我不知道这是按规范,按惯例还是只是一个 promise 引擎与另一个 promise 引擎的实现细节。我想说的是,如果订单对您至关重要,那么您应该通过按特定顺序链接 promise 来强制执行订单,而不要依靠谁赢得了比赛的第一名。

    无论如何,在您的情况下,这是一场调度竞赛,您正在运行的引擎决定运行接下来在第5行定义的内部.then()处理程序,因此您会看到在的第6行上指定的7 "C"。然后,它不返回任何内容,因此该Promise的解析值变为undefined

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

    回到调度程序中,它决定在的第7行上运行.then()处理程序,您会得到8 undefined。该约定中存在undefined,因为该链中的先前.then()处理程序未返回任何内容,因此其返回值为undefined,因此这是该时刻约定链的已解析值。

    至此,到目前为止的输出为:
    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

    然后,最后,setTimeout()开始运行,最终输出为:
    1
    2 "A"
    3 "B"
    7 "C"
    4 "B"
    8 undefined
    9 "D"
    5 undefined
    10 undefined
    6
    

    如果要试图准确预测其运行顺序,那么将有两个主要问题。
  • 与待处理的.then()调用相比,待处理的setTimeout()处理程序如何确定优先级。
  • promise 引擎如何决定对所有正在等待运行的.then()处理程序进行优先级排序。根据此代码的结果,它不是FIFO。

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

    对于第二个问题,我看到了一些讨论,讨论了应如何确定不同嵌套级别的未决.then()处理程序的优先级。我不知道该讨论是否在规范中得到解决。我更喜欢以那种程度的细节对我来说无关紧要的方式进行编码。如果我在乎异步操作的顺序,则可以链接我的promise链来控制顺序,并且此实现细节级别不会以任何方式影响我。如果我不在乎顺序,那么我也不在乎顺序,因此实现细节的级别也不会影响我。即使是在某种规范中,似乎您也不应在许多不同的实现(不同的浏览器,不同的Promise引擎)中信任详细信息的类型,除非您在要运行的所有地方都对其进行了测试。因此,当您的 promise 链不同步时,我建议不要依赖特定的执行顺序。

    您可以像这样链接所有 promise 链(返回内部 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
    

    而且,由于promise已全部链接在一起,所以promise的顺序全部由代码定义。剩下的唯一实现细节是setTimeout()的计时,在您的示例中,这是最后一个在所有未决.then()处理程序之后的计时。

    编辑:

    通过检查Promises/A+ specification,我们发现:

    2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

    ....

    3.1 Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.



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

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

    我还没有找到支持它的任何规范,但是Difference between microtask and macrotask within an event loop context这个问题的答案说明了事情在具有宏任务和微任务的浏览器中是如何工作的。

    仅供引用,如果您想了解有关微任务和宏任务的更多信息,这是一篇有关该主题的有趣引用文章:Tasks, microtasks, queues and schedules

    关于javascript - JavaScript promises中的执行顺序是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36870467/

    相关文章:

    javascript - 从 this.parent 获取 ID

    javascript - IE 上的应用缓存,Edge 在关闭浏览器后无法工作

    angularjs - 为什么我在测试 promise 内的代码时会丢失调用上下文( Angular + Jasmine )

    javascript - 这段代码如何使用 promises 顺序获取 url 并将其顺序加载到 html 中?

    javascript - 以下 JS 代码不适用于函数中的 .length 属性

    javascript - ES6 导入/导出语法的困难

    javascript - NodeJS Promise 行为查询

    javascript - Promise 返回值然后回调函数

    JavaScript ES6 promise

    javascript - 在没有 I/O 的情况下,javascript(在浏览器中)的异步/ promise 是否有益?