评论
首先,在.then()
处理程序中运行承诺并且不从.then()
回调中返回这些承诺会创建一个完全不同的未连接的承诺序列,与任何父承诺都不同步。通常,这是一个错误,事实上,一些承诺引擎在您这样做时实际上会发出警告,因为这几乎永远不是所需的行为。唯一需要这样做的时间是当您正在执行某种“点火和忘记”操作时,您不关心错误,也不关心与世界其他部分的同步。
因此,在
.then()
处理程序中的所有
Promise.resolve()
承诺都会创建独立于父链的新承诺链。对于实际异步操作,您没有非连接的独立承诺链的确定行为。这有点像并行启动四个ajax调用。您不知道哪一个会先完成。现在,由于那些
Promise.resolve()
处理程序中的所有代码恰好是同步的(因为这不是真实世界的代码),所以您可能会得到一致的行为,但这不是承诺的设计重点,因此我不会花太多时间尝试弄清楚哪个只运行同步代码的Promise链将首先完成。在现实世界中,这并不重要,因为如果顺序很重要,那么您不会以这种方式留下事情的机会。
总结
.then()
处理程序在当前执行线程完成后异步调用(正如Promises/A+规范所说,当JS引擎返回到“平台代码”时)。即使是同步解决的承诺,例如Promise.resolve().then(...)
,也是如此。这样做是为了编程一致性,以便无论承诺是立即解决还是稍后解决,.then()
处理程序始终被异步调用。这可以防止一些时间错误,并使调用代码更容易看到一致的异步执行。
没有规范确定setTimeout()
与计划的.then()
处理程序的相对顺序,如果两者都排队并准备好运行。在您的实现中,挂起的.then()
处理程序总是在挂起的setTimeout()
之前运行,但Promises/A+规范规定这不是确定的。它说.then()
处理程序可以以许多方式安排,其中一些将在挂起的setTimeout()
调用之前运行,而另一些可能在挂起的setTimeout()
调用之后运行。例如,Promises/A+规范允许使用setImmediate()
或setTimeout()
安排.then()
处理程序,前者将在挂起的setTimeout()
调用之前运行,后者将在挂起的setTimeout()
调用之后运行。因此,您的代码不应依赖于任何顺序。
多个独立的Promise链没有可预测的执行顺序,您不能依赖于任何特定的顺序。这就像并行发射四个ajax调用,您不知道哪一个会首先完成。
如果执行顺序很重要,请不要创建依赖于微小实现细节的竞争。而是链接承诺链以强制执行特定的执行顺序。
通常情况下,您不希望在未从处理程序返回的情况下创建独立的承诺链。除了罕见的无需错误处理的情况外,这通常是一个错误。
逐行分析
下面是你代码的分析。我添加了行号并清理了缩进,以便更容易讨论:
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);
第一行开始一个Promise链并附加了一个
.then()
处理程序。由于
Promise.resolve()
立即解决,Promise库将在此JavaScript线程完成后安排第一个
.then()
处理程序运行。在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()
处理程序运行。这是您正在创建另一个独立的承诺链并引入通常是错误的行为的地方。
第5行,创建了一个新的Promise链。它解决了初始承诺,然后安排两个
.then()
处理程序在当前执行线程完成时运行。在当前执行线程中是第10行的
console.log(3, a)
,这就是为什么你看到下一个的原因。然后,这个执行线程完成并返回调度程序以查看接下来要运行什么。
现在我们有几个
.then()
处理程序在队列中等待下一步运行。有我们刚刚在第5行安排的处理程序,还有更高级别链中的下一个处理程序在第12行。如果你在
第5行这样做:
return Promise.resolve(...).then(...)
如果你将这些promise链接在一起,它们将按顺序协调。但是,由于没有返回promise值,您启动了一个全新的promise链,它与外部更高级别的promise不协调。在您的特定情况下,promise调度程序决定先运行更深层嵌套的
.then()
处理程序。老实说,我不知道这是根据规范、约定还是一个promise引擎与另一个之间的实现细节。如果顺序对您很重要,那么您应该通过按特定顺序链接promises来强制排序,而不是依赖于谁赢得了第一场比赛。
无论如何,在您的情况下,这是一个调度竞赛,您正在运行的引擎决定接下来运行定义在第5行的内部
.then()
处理程序,因此您可以看到在
第6行指定的
7“C”
。然后它返回空,因此该promise的已解决值变为
undefined
。
回到调度程序,它在第12行上运行.then()
处理程序。这又是一个竞争,即等待运行的第7行上的处理程序与之竞争。我不知道为什么它选择了其中一个而不是另一个,除了说这可能是不确定的或因承诺引擎而异,因为代码没有指定顺序。无论如何,第12行上的.then()
处理程序开始运行。这再次创建了一个新的独立或不同步的承诺链,就像前一个一样。它再次安排.then()
处理程序,然后您会从该.then()
处理程序中的同步代码获得4 "B"
。所有同步代码都在该处理程序中完成,现在它返回调度程序以获取下一个任务。
在调度程序中,它决定在
第7行运行
.then()
处理程序,你会得到
8 undefined
。那里的 promise 是
undefined
,因为该链中之前的
.then()
处理程序没有返回任何内容,因此其返回值为
undefined
,因此这是该 promise 链的解析值在该点处。
此时,到目前为止的输出为:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
再次强调,所有同步代码都是这样完成的,它会返回到调度程序,然后决定运行第13行定义的.then()
处理程序。该处理程序运行并输出9 "D"
,然后又返回到调度程序。
与之前嵌套的Promise.resolve()
链一致,调度程序选择运行下一个外部.then()
处理程序,该处理程序在第19行定义。它运行并输出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()
调用之间的优先级如何。
- 在等待运行的多个
.then()
处理程序中,Promise 引擎如何决定优先级。根据您使用此代码的结果,它不是先进先出(FIFO)的。
对于第一个问题,我不知道这是否符合规范或者只是 Promise 引擎/JS 引擎中的一种实现选择,但你报告的实现似乎优先处理所有未决的
.then()
处理程序,然后再处理任何
setTimeout()
调用。你的情况有点奇怪,因为除了指定
.then()
处理程序之外,你没有任何实际的异步 API 调用。如果你在这个 Promise 链的开头有任何实际需要花费时间来执行的异步操作,那么你的
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 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 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+规范,我们发现以下内容:
2.2.4 必须在执行上下文堆栈仅包含平台代码时才能调用 onFulfilled 或 onRejected。[3.1]
....
3.1 这里的“平台代码”指引擎、环境和承诺实现代码。实际上,这个要求确保 onFulfilled 和 onRejected 在 then 被调用的事件循环后异步执行,并使用新的堆栈。这可以使用“宏任务”机制(如 setTimeout 或 setImmediate)或“微任务”机制(如 MutationObserver 或 process.nextTick)来实现。由于承诺实现被认为是平台代码,因此它本身可能包含一个任务调度队列或“跳板”,其中处理程序被调用。
这段文字表明,
.then()
处理程序必须在调用栈返回到平台代码后异步执行,但完全由实现决定如何执行,无论是使用类似于
setTimeout()
的宏任务还是类似于
process.nextTick()
的微任务。因此,根据此规范,它是不确定的,不应该依赖它。
我在ES6规范中没有找到关于宏任务、微任务或承诺
.then()
处理程序与
setTimeout()
相关的时间信息。这也许并不奇怪,因为
setTimeout()
本身不是ES6规范的一部分(它是一个主机环境函数,而不是语言特性)。
我没有找到任何支持这一观点的规范,但这个问题的答案
Difference between microtask and macrotask within an event loop context解释了在浏览器中宏任务和微任务通常是如何工作的。
如果您想了解更多关于微任务和宏任务的信息,这里有一篇有趣的参考文章:Tasks, microtasks, queues and schedules。
return
值工作,它们并不神奇 :) 如果你不从then
返回,它就不会起作用。如果你在添加函数时添加return
,它将按你的预期工作。这样的重复有大约100个 - 我们只需等待bergi或jfriend指出一个好的。 - Benjamin Gruenbaumreturn
,它将隐式返回undefined
。 - Srle