JavaScript Promise 的执行顺序是什么?

IT技术 javascript promise es6-promise
2021-02-10 04:59:58

我想了解以下使用 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 ... 记录的顺序?

3个回答

评论

首先,在.then()处理程序中运行 Promise并且不从.then()回调中返回这些 Promise会创建一个全新的未附加的 Promise 序列,该序列不会以任何方式与父 Promise 同步。通常,这是一个错误,实际上,当您这样做时,一些Promise引擎实际上会发出警告,因为这几乎不是您想要的行为。唯一一次你想要这样做的时候是当你在做某种“即发即忘”的操作时,你不关心错误,也不关心与世界其他地方的同步。

所以,所有Promise.resolve()的内部Promise.then()处理程序创建一个独立于母公司链的运行新的无极链。对于实际的异步操作,对于非连接的、独立的Promise链,您没有确定的行为。这有点像并行启动四个 ajax 调用。你不知道哪一个会先完成。现在,由于您在这些Promise.resolve()处理程序中的所有代码碰巧都是同步的(因为这不是真实世界的代码),那么您可能会获得一致的行为,但这不是 Promise 的设计点,所以我不会花太多时间尝试找出哪个仅运行同步代码的 Promise 链将首先完成。在现实世界中,这并不重要,因为如果顺序很重要,那么您就不会以这种方式让事情发生。

概括

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

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

  3. 多个独立的 Promise 链没有可预测的执行顺序,您不能依赖任何特定的顺序。这就像并行启动四个 ajax 调用,而您不知道哪个会先完成。

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

  5. 您通常不想在.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 库中,所有.then()处理程序在当前执行线程完成后以及当 JS 返回到事件循环时异步调用。这意味着该线程中的任何其他同步代码(例如您console.log(1)将在接下来运行),这就是您所看到的。

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

由于setTimeout()也在这个初始执行线程中,因此它会运行并因此调度一个计时器。

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

据我所知,不能保证哪个首先出现一个setTimeout(fn, 0)或一个.then()处理程序,它们都被安排在这个执行线程之后立即运行。 .then()处理程序被认为是“微任务”,因此它们在setTimeout(). 但是,如果您需要一个特定的订单,那么您应该编写保证订单的代码,而不是依赖于这个实现细节。

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

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

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

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

return Promise.resolve.then(...)

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

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

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

回到调度程序,它决定.then()第 7 行运行处理程序,你得到8 undefined. 那里的Promise是undefined因为该.then()链中的前一个处理程序没有返回任何内容,因此其返回值为undefined,因此这是当时Promise链的已解析值。

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

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

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

与之前嵌套的Promise.resolve()一致,调度选择运行第19 行.then()定义的下一个外部处理程序它运行并得到输出再次是因为该链中的前一个处理程序没有返回值,因此 Promise 的解析值是5 undefinedundefined.then()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

如果要尝试准确预测这将运行的顺序,那么将有两个主要问题。

  1. .then()处理的处理程序与同样待处理的setTimeout()调用相比如何区分优先级

  2. Promise引擎如何决定对多个.then()等待运行的处理程序进行优先级排序根据您使用此代码的结果,它不是 FIFO。

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

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


您可以通过像这样链接所有Promise链来使订单 100% 确定(返回内部Promise,以便它们链接到父链中):

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+ 规范后,我们发现:

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

....

3.1 这里的“平台代码”是指引擎、环境和promise实现代码。在实践中,这个要求确保 onFulfilled 和 onRejected 异步执行,在调用 then 的事件循环之后,并使用新的堆栈。这可以通过“宏任务”机制(例如 setTimeout 或 setImmediate)或“微任务”机制(例如 MutationObserver 或 process.nextTick)来实现。由于promise实现被认为是平台代码,它本身可能包含一个任务调度队列或“trampoline”,在其中调用处理程序。

这表示.then()处理程序必须在调用堆栈返回到平台代码后异步执行,但完全取决于实现如何准确地执行该操作,无论它是使用宏任务setTimeout()还是微任务完成process.nextTick()因此,根据本规范,它不是确定的,不应依赖。

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

我还没有找到任何规范来支持这一点,但是这个问题的答案在事件循环上下文中微任务和宏任务之间的差异解释了事物在具有宏任务和微任务的浏览器中的工作方式。

仅供参考,如果您想了解有关微任务和宏任务的更多信息,这里有一篇关于该主题的有趣参考文章:任务、微任务、队列和计划

关于待处理的定时添加的信息.then()处理程序与setTimeout()Promise/ A +规范
2021-03-17 04:59:58
“据我所知,不能保证谁先到……”,这完全取决于Promise的执行情况。setTimeout实际上有一个最小超时延迟(类似于15msIIRC),但 Promises可能会使用setImmediate,这当然会在同时设置计时器之前添加到 JS 事件循环中。Promise 实现通常使用setTimeout(fn, 0),然后将按照它们的最小计时器到期的顺序进行解析,这将按照它们被调用的顺序发生。
2021-03-22 04:59:58
@zzzzBov - 所以,为了 100% 清楚,您同意没有确定相对顺序的规范setTimeout()- 这取决于实现。并且,该规范甚至允许使用setTimeout()用于调度.then()处理程序,这些处理程序可以更改此处所见的顺序。
2021-03-31 04:59:58
是的,只是详细说明了我认为与该部分相关的内容。
2021-04-05 04:59:58
另外,我应该提到我没有投票。我认为你得到了一个很好的答案。
2021-04-07 04:59:58

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

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

这意味着,如果您使用 AJAX 之类的东西,您无法控制请求的完成顺序,那么您的Promise每次都可以以不同的顺序执行。

实际上,在大多数浏览器中,.then回调在一个微任务队列上执行,该队列在当前 run-to-completion 结束时被清空,在主事件循环开始之前。
2021-03-23 04:59:58

HTML 事件循环包含各种任务队列和一个微任务队列。

在每个事件循环的迭代开始时,将从其中一个任务队列中取出一个新任务,这就是俗称的“宏任务”。

然而,每个事件循环迭代不会只访问一次微任务队列。每次清空 JS 调用堆栈时都会访问它。这意味着它可以在单个事件循环迭代中被多次访问(因为在一个事件循环迭代中执行的所有任务都不是来自任务队列)。

该微任务队列的另一个特殊之处在于,在队列出队时排队的微任务将立即在同一检查点中执行,而不会让事件循环执行任何其他操作。

在您的示例中,所有链接或在第一个内部的所有内容Promise.resolve("A")都是同步的,或者排队新的微任务,而实际上没有任何排队(宏)任务的内容。
这意味着当 Event Loop 进入微任务检查点执行第一个 Promise react回调时,它不会离开那个微任务检查点,直到最后一个排队的微任务执行完毕。
所以你的超时在这里是无关紧要的,它会在所有这些 Promise react之后被执行。

澄清了这一点,我们现在可以遍历您的代码并用queueMicrotask(callback)它将调用的底层替换每个 Promise react然后很清楚执行顺序是什么:

或者,如果我们提取链外的每个回调:

现在我应该注意,处理已经解决(或立即解决)的 Promise 不是你每天都应该看到的,所以要注意,一旦这些 Promise react之一实际上绑定到一个异步任务,顺序就不会可靠此外,由于不同的(宏)任务队列可能具有由 UA 定义的不同优先级。
但是,我认为理解微任务队列如何工作以避免阻塞事件循环仍然很重要,因为它期望Promise.resolve()会让事件循环呼吸,它不会。

壮观的答案,尤其是。随着queueMicrotask电话。将立即解析异步函数与实际执行任务的异步函数混合使用时非常有用。
2021-03-20 04:59:58