JavaScript Promise 中的执行顺序是啥?

Posted

技术标签:

【中文标题】JavaScript Promise 中的执行顺序是啥?【英文标题】:What is the order of execution in JavaScript promises?JavaScript Promise 中的执行顺序是什么? 【发布时间】:2016-08-20 14:36:30 【问题描述】:

我想了解以下使用 javascript Promise 的 sn-p 的执行顺序。

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"...

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

为什么不是 1 2 3 4 ... 记录的顺序?

【问题讨论】:

Promises 由 return 值工作,它们并不神奇 :) 如果你不从 then 返回它就行不通。如果您添加return 来添加您的功能,它将按您的预期工作。大约有 100 个重复项 - 将等待 bergi 或 jfriend 指出一个好的。 “我很好奇执行顺序 1 2 3 7...不是值 'A', 'B'...” 然后去掉无关信息和问题中的代码。 如果你没有从 Promise 中显式地 return,它会隐式地返回 undefined 【参考方案1】:

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

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

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

【讨论】:

实际上,在大多数浏览器中,.then 回调在一个微任务队列上执行,该队列在当前运行到完成结束时,在主事件循环开始之前被清空。【参考方案2】:

评论

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

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

总结

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

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

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

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

    您通常不希望在 .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);

第 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() 之前首先运行并不让我感到惊讶。但是,如果您需要特定的订单,那么您应该编写保证订单的代码,而不是依赖这个实现细节。

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

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

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

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

return Promise.resolve.then(...)

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

无论如何,在您的情况下,这是一场调度竞赛,您正在运行的引擎决定运行下一个在第 5 行定义的内部 .then() 处理程序,因此您会看到在 第 6 行指定的 7 "C" /强>。然后它什么也不返回,所以这个 Promise 的解析值变成了undefined

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

回到调度程序,它决定在 第 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 引擎/JS 引擎中的实现选择,但您报告的实现似乎优先考虑所有待处理的 .then() 处理程序,然后是任何 @ 987654400@ 电话。您的情况有点奇怪,因为除了指定 .then() 处理程序之外,您没有实际的异步 API 调用。如果您有任何异步操作在此承诺链开始时实际上需要任何实时执行,那么您的 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 在事件之后异步执行 循环调用 then ,并使用新堆栈。这可以是 使用“宏任务”机制实现,例如 setTimeout 或 setImmediate,或使用“微任务”机制,例如 MutationObserver 或 process.nextTick。自承诺实施以来 被认为是平台代码,它本身可能包含一个任务调度 调用处理程序的队列或“蹦床”。

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

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

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

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

【讨论】:

添加了有关来自Promises/A+ specification 的待处理.then() 处理程序与setTimeout() 时间的信息。 “据我所知,不能保证先到先得……”,这完全取决于承诺的实现。 setTimeout 实际上有一个最小的超时延迟(类似于15ms IIRC),但是 Promises 可能使用 setImmediate,它当然会在设置相同的计时器之前添加到 JS 事件循环中时间。 Promise 实现通常使用setTimeout(fn, 0),然后将按照它们的最小计时器到期的顺序进行解析,这将按照它们被调用的顺序发生。 另外,我应该提到我没有投反对票。我认为你的答案很好。 @zzzzBov - 所以,为了 100% 清楚,您同意没有规范来确定 setTimeout() 的相对顺序 - 这取决于实现。而且,该规范甚至允许使用setTimeout() 来调度.then() 处理程序,这可能会改变这里看到的顺序。 是的,只是详细说明,包括我认为与该部分相关的部分。【参考方案3】:

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

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

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

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

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

澄清后,我们现在可以遍历您的代码并将每个 Promise 响应替换为它将调用的底层 queueMicrotask(callback)。那么执行顺序是什么就很清楚了:

queueMicrotask(function(a)  // first callback
  console.log(2, a, 1);

  queueMicrotask(function(a)  // second callback
    // new branch
    queueMicrotask(function(a)  // third callback
      console.log(7, a, 3);
      queueMicrotask(function(a)  // fifth callback
        console.log(8, a, 5);
      );
    .bind(null, "C"));

    // synchronous (in second callback)
    console.log(3, a, 2);

    //main branch
    queueMicrotask(function(a)  // fourth callback (same level as third, but called later)
      // new branch
      queueMicrotask(function(a)  // sixth callback
        console.log(9, a, 6);
        queueMicrotask(function(a)  // eighth callback
          console.log(10, a, 8);
        );
      .bind(null, "D"));

      // synchronous
      console.log(4, a, 4);

      // main branch
      queueMicrotask(function(a)  // seventh callback
        console.log(5, a, 7);
      );
    .bind(null, a))
  .bind(null, "B"));
.bind(null, "A"));

// synchronous
console.log(1);
// irrelevant
setTimeout(function() 
  console.log(6);
);

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

function first(a) 
  console.log(2, a, 1);
  queueMicrotask(second.bind(null, "B"));

function second(a) 
  queueMicrotask(third.bind(null, "C"));
  console.log(3, a, 2);
  queueMicrotask(fourth.bind(null, a));

function third(a) 
  console.log(7, a, 3);
  queueMicrotask(fifth);

function fourth(a) 
  queueMicrotask(sixth.bind(null, "D"));
  console.log(4, a, 4);
  queueMicrotask(seventh);

function fifth(a) 
  console.log(8, a, 5);
;
function sixth(a) 
  console.log(9, a, 6);
  queueMicrotask(eighth);

function seventh(a) 
  console.log(5, a, 7);

function eighth(a) 
  console.log(10, a, 8);

queueMicrotask(first.bind(null, "A"));

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

【讨论】:

壮观的答案,尤其是。与queueMicrotask 通话。在将立即解析异步函数与实际执行任务的异步函数混合使用时非常有用。

以上是关于JavaScript Promise 中的执行顺序是啥?的主要内容,如果未能解决你的问题,请参考以下文章

javascript accumulate():按顺序执行promises或promise-returns函数数组,并使用给定的累加器减少结果

javaScript-promise

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

promise执行顺序

JS 中关于Promise的用法,状态,执行顺序详解,面试可用(原创)

JavaScript——异步操作以及Promise 的使用