JavaScript 承诺的执行顺序是什么?

What is the order of execution in JavaScript promises?

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

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 ... 不是记录的顺序?

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

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

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

评论

首先,运行在 .then() 处理程序中承诺而不是 return 来自 .then() 回调的承诺会创建一个全新的独立承诺序列不以任何方式与父承诺同步。通常,这是一个错误,事实上,当您这样做时,一些承诺引擎实际上会发出警告,因为这几乎不是期望的行为。唯一一次人们想要这样做的时候是当你在做一些你不关心错误并且不关心与世界其他地方同步的操作时。

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

总结

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

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

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

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

  5. 您通常不希望在 .then() 处理程序中创建不是从处理程序 return 编辑的独立承诺链。这通常是一个错误,除了在极少数情况下没有错误处理的情况下就忘记了。

逐行分析

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

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

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

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

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

据我所知,无法保证 setTimeout(fn, 0).then() 处理程序在该线程执行后立即调度到 运行。 .then() 处理程序被认为是“微任务”,因此它们 运行 先于 setTimeout() 并不让我感到惊讶。但是,如果您需要特定的顺序,那么您应该编写保证顺序的代码,而不是依赖于此实现细节。

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

的输出 2 "A"

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

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

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

return Promise.resolve.then(...)

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

无论如何,在你的情况下,这是一场调度竞赛,你正在使用的引擎 运行ning 决定 运行 接下来在第 5 行定义的内部 .then() 处理程序,因此你请参阅 第 6 行 中指定的 7 "C"。然后 return 什么都没有,所以这个承诺的解析值变成 undefined.

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

回到调度程序,它决定 运行 .then() 处理程序 第 7 行 并且你得到 8 undefined。那里的承诺是 undefined 因为该链中的前一个 .then() 处理程序没有 return 任何东西,因此它的 return 值是 undefined,因此这是已解决的那时承诺链的价值。

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

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

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

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

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

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

此时,只有一个 .then() 处理程序计划为 运行,因此它 运行 是 第 15 行 [=221= 定义的] 接下来你会得到输出 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 engine/JS 引擎中的一个实现选择,但您报告的实现似乎优先考虑所有未决 .then() 处理程序在任何 setTimeout() 调用之前。您的情况有点奇怪,因为除了指定 .then() 处理程序之外,您没有实际的异步 API 调用。如果您有任何异步操作实际上需要任何实时时间才能在此承诺链的开头执行,那么您的 setTimeout() 将在真正的异步操作的 .then() 处理程序之前执行,因为真正的异步操作需要实际时间来执行。所以,这是一个人为的例子,不是真实代码的通常设计案例。

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


你可以通过 link 像这样 linking 你所有的承诺链来使订单 100% 确定(returning 内部承诺,所以它们 link 进入父链) :

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() 处理程序必须在调用堆栈 returns 之后异步执行到平台代码,但完全由实现留给实现,无论它是用宏完成的—— setTimeout() 之类的任务或 process.nextTick() 这样的微任务。因此,根据此规范,它不是确定的,不应依赖。

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

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

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

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() 会让事件循环呼吸,它不会。