评论
首先,在.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。