评论
首先,在.then()
处理程序中运行 Promise并且不从.then()
回调中返回这些 Promise会创建一个全新的未附加的 Promise 序列,该序列不会以任何方式与父 Promise 同步。通常,这是一个错误,实际上,当您这样做时,一些Promise引擎实际上会发出警告,因为这几乎不是您想要的行为。唯一一次你想要这样做的时候是当你在做某种“即发即忘”的操作时,你不关心错误,也不关心与世界其他地方的同步。
所以,所有Promise.resolve()
的内部Promise.then()
处理程序创建一个独立于母公司链的运行新的无极链。对于实际的异步操作,对于非连接的、独立的Promise链,您没有确定的行为。这有点像并行启动四个 ajax 调用。你不知道哪一个会先完成。现在,由于您在这些Promise.resolve()
处理程序中的所有代码碰巧都是同步的(因为这不是真实世界的代码),那么您可能会获得一致的行为,但这不是 Promise 的设计点,所以我不会花太多时间尝试找出哪个仅运行同步代码的 Promise 链将首先完成。在现实世界中,这并不重要,因为如果顺序很重要,那么您就不会以这种方式让事情发生。
概括
.then()
在当前执行线程完成后(如 Promises/A+ 规范所说,当 JS 引擎返回“平台代码”时)异步调用所有处理程序。即使对于同步解析的 Promise 也是如此,例如Promise.resolve().then(...)
. 这样做是为了编程一致性,以便.then()
无论Promise是立即解决还是稍后解决,都始终异步调用处理程序。这可以防止一些计时错误,并使调用代码更容易看到一致的异步执行。
如果两者都已排队并准备好运行,则没有确定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 库中,所有.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 undefined
undefined
.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
如果要尝试准确预测这将运行的顺序,那么将有两个主要问题。
待.then()
处理的处理程序与同样待处理的setTimeout()
调用相比如何区分优先级。
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 规范的一部分(它是宿主环境功能,而不是语言功能)。
我还没有找到任何规范来支持这一点,但是这个问题的答案在事件循环上下文中微任务和宏任务之间的差异解释了事物在具有宏任务和微任务的浏览器中的工作方式。
仅供参考,如果您想了解有关微任务和宏任务的更多信息,这里有一篇关于该主题的有趣参考文章:任务、微任务、队列和计划。