I.R*_*.R. 30 javascript promise es6-promise
我想向自己解释下面使用javascript promises的代码片段的执行顺序.
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);
Run Code Online (Sandbox Code Playgroud)
结果是:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
Run Code Online (Sandbox Code Playgroud)
我很好奇执行顺序1 2 3 7 ...而不是值'A','B'......
我的理解是,如果一个promise得到解决,'then'函数将被放入浏览器事件队列中.所以我的期望是1 2 3 4 ......
@ jfriend00谢谢,非常感谢您的详细解释!这真是一项巨大的工作!
jfr*_*d00 77
评论
首先,在.then()处理程序内运行promises .then()而不从回调中返回这些promise 会创建一个全新的未附加的promise序列,它不会以任何方式与父promises同步.通常,这是一个错误,事实上,一些承诺引擎实际上会在您这样做时发出警告,因为它几乎不是所希望的行为.人们想要做的唯一一次就是当你做某种火灾并忘记操作时你不关心错误而你不关心与世界其他地方的同步.
因此,处理程序Promise.resolve()内的所有承诺都会.then()创建新的Promise链,这些链独立于父链运行.您没有确定的行为.这有点像并行启动四个ajax调用.你不知道哪一个会先完成.现在,由于这些Promise.resolve()处理程序中的所有代码都是同步的(因为这不是真实的代码),那么你可能会得到一致的行为,但这不是承诺的设计点所以我不会花太多时间尝试找出只运行同步代码的Promise链将首先完成.在现实世界中,这并不重要,因为如果秩序很重要,那么你就不会以这种方式把事情搞得一团糟.
摘要
.then()在当前执行线程完成后,所有处理程序都是异步调用的(正如Promises/A +规范所说,当JS引擎返回"平台代码"时).即使对于同步解决的承诺,例如,也是如此Promise.resolve().then(...).这样做是为了编程一致性,因此.then()无论是立即解决还是稍后解决,都会异步调用处理程序.这可以防止一些计时错误,并使调用代码更容易看到一致的异步执行.
如果两个队列都已排队并准备运行,则没有规范可确定setTimeout()与预定.then()处理程序的相对顺序.在您的实现中,挂起的.then()处理程序总是在挂起之前运行setTimeout(),但Promises/A +规范说明这不是确定的.它表示.then()处理程序可以通过多种方式进行调度,其中一些方法可以在挂起的setTimeout()调用之前运行,其中一些可能在挂起的setTimeout()调用之后运行.例如,Promises/A +规范允许.then()处理程序使用setImmediate()哪个将在挂起的setTimeout()调用之前运行,或者setTimeout()在挂起的setTimeout()调用之后运行.因此,您的代码根本不应该依赖于该顺序.
多个独立的Promise链没有可预测的执行顺序,您不能依赖任何特定的顺序.这就像并行发射四个ajax调用,你不知道哪一个会先完成.
如果执行顺序很重要,请不要创建依赖于详细实现细节的竞赛.相反,链接承诺链以强制执行特定的执行顺序.
您通常不希望在.then()处理程序中创建未从处理程序返回的独立promise链.这通常是一个错误,除非在极少数情况下发生火灾,忘记没有错误处理.
逐行Analsysis
所以,这是对你的代码的分析.我添加了行号并清理了缩进,以便更容易讨论:
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);
Run Code Online (Sandbox Code Playgroud)
第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()定义的处理程序接下来运行.因此,你可以看到输出从.2 "A"console.log(2, a)
接下来,由于前一个.then()处理程序返回一个普通值,因此该承诺被认为已解析,因此第4行.then()定义的处理程序将运行.这是您创建另一个独立的承诺链并引入通常是错误的行为的地方.
第5行,创建一个新的Promise链.它解析了初始promise,然后安排两个.then()处理程序在当前执行线程完成时运行.在当前的执行线程是console.log(3, a)在线10,这就是为什么你看到下一个.然后,这个执行线程结束,然后返回调度程序,看看接下来要运行什么.
我们现在.then()在队列中有几个处理程序等待下一个运行.我们刚刚在第5行安排了一个,第12行的更高级链中有下一个.如果你在第5行完成了这个:
return Promise.resolve.then(...)
Run Code Online (Sandbox Code Playgroud)
然后你会将这些承诺联系在一起,它们将按顺序协调.但是,通过不返回承诺值,您开始了一个全新的承诺链,而不是与外部的更高级别承诺协调.在您的特定情况下,promise调度程序决定.then()接下来运行更深层次的嵌套处理程序.我真的不知道这是通过规范,按惯例还是只是一个承诺引擎与另一个承诺引擎的实现细节.我会说,如果订单对你很重要,那么你应该通过按特定顺序链接承诺来强制执行订单,而不是依靠谁赢得竞争首先运行.
无论如何,在你的情况下,它是一个调度竞赛,你正在运行的引擎决定运行.then()在第5行定义的内部处理程序,因此你会7 "C"在第6行看到指定的.然后它不返回任何内容,因此该承诺的解析值变为undefined.
回到调度程序,它.then()在第12行运行处理程序.这又是该.then()处理程序和第7行之间的竞争,它也在等待运行.我不知道为什么它在这里选择一个而不是说它可能是不确定的或因每个承诺引擎而异,因为代码没有指定顺序.在任何情况下,第12行中的.then()处理程序开始运行.这再次创建了一个新的独立或不同步的承诺链线.它再次调度处理程序,然后从该处理程序中的同步代码中获取.所有同步代码都在该处理程序中完成,现在,它将返回到调度程序以执行下一个任务..then()4 "B".then()
回到调度程序,它决定.then()在第7行运行处理程序,然后得到8 undefined.这是undefined因为该.then()链中的前一个处理程序没有返回任何内容,因此它的返回值是undefined,因此这是该点的promise链的已解析值.
此时,到目前为止的输出是:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
Run Code Online (Sandbox Code Playgroud)
同样,所有同步代码都已完成,因此它再次返回调度程序,并决定运行第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
Run Code Online (Sandbox Code Playgroud)
此时,只有一个.then()处理程序计划运行,因此它运行第15行定义的处理程序,然后10 undefined接下来获得输出.
然后,最后,setTimeout()获得运行,最终输出是:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
Run Code Online (Sandbox Code Playgroud)
如果一个人试图准确地预测这将会发生的顺序,那么将会有两个主要问题.
挂起的.then()处理程序如何优先处理,还有待处理的setTimeout()调用.
promise引擎如何决定优先.then()处理等待运行的多个处理程序.根据此代码的结果,它不是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 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);
Run Code Online (Sandbox Code Playgroud)
这会在Chrome中提供以下输出:
1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6
Run Code Online (Sandbox Code Playgroud)
并且,由于承诺全部链接在一起,承诺顺序全部由代码定义.作为一个实现细节留下的唯一的东西是setTimeout()在所有挂起的.then()处理程序之后,在你的例子中,最后的时间.
编辑:
在检查Promises/A +规范后,我们发现:
2.2.4 onFulfilled或onRejected在执行上下文堆栈仅包含平台代码之前不得调用.[3.1].
....
3.1这里"平台代码"表示引擎,环境和承诺实现代码.实际上,这个要求确保onFulfilled和onRejected异步执行,然后调用事件循环,然后调用新堆栈.这可以使用诸如setTimeout或setImmediate之类的"宏任务"机制,或者使用诸如MutationObserver或process.nextTick之类的"微任务"机制来实现.由于promise实现被认为是平台代码,因此它本身可能包含一个任务调度队列或"trampoline",其中调用处理程序.
这表示.then()处理程序必须在调用堆栈返回平台代码后异步执行,但是完全将它完全留给实现,无论是使用像宏程序setTimeout()还是像微任务一样完成它process.nextTick().因此,根据此规范,它不是确定的,不应该依赖.
我没有找到有关宏任务,微任务或与ES6规范.then()相关的承诺处理程序的时间的信息setTimeout().这可能并不奇怪,因为setTimeout()它本身不是ES6规范的一部分(它是主机环境功能,而不是语言功能).
我没有找到任何规范来支持这一点,但是这个问题的答案在事件循环上下文中微任务和宏任务之间的差异解释了在具有宏任务和微任务的浏览器中事物是如何工作的.
仅供参考,如果您想了解有关微任务和宏任务的更多信息,请参阅以下有关该主题的有趣参考文章:任务,微任务,队列和日程安排.
| 归档时间: |
|
| 查看次数: |
11732 次 |
| 最近记录: |