Promise的队列与setTimeout的队列有何关联?
27 个回答
先把问题贴一下:
Promise的队列与setTimeout的队列的有何关联?
setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i<10000 ; i++ ){ i==9999 && resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3);
为什么结果是:
1,2,3,5,4
而不是:
1,2,3,4,5
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
不想看故事的,直接看结论在这。
原因:
有一个事件循环,但是任务队列可以有多个。
整个script代码,放在了macrotask queue中,setTimeout也放入macrotask queue。
但是,promise.then放到了另一个任务队列microtask queue中。
这两个任务队列执行顺序如下,取1个macrotask queue中的task,执行之。
然后把所有microtask queue顺序执行完,再取macrotask queue中的下一个任务。
代码开始执行时,所有这些代码在macrotask queue中,取出来执行之。
后面遇到了setTimeout,又加入到macrotask queue中,
然后,遇到了promise.then,放入到了另一个队列microtask queue。
等整个execution context stack执行完后,
下一步该取的是microtask queue中的任务了。
因此promise.then的回调比setTimeout先执行。
(下面,多图,多坑,多水。。。危险。。。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
本来我是这样以为的:
因为promise.then既可以在pending的时候注册回调,也可以在fullfill状态注册回调。
在pending的时候,注册的是异步回调。而在fullfill状态,注册的是同步回调。
只有异步回调才会依赖任务队列,而同步回调马上执行。
题主这种情况,注册的是同步回调。
注意,new Promise是同步的,会马上执行function参数中的事情。
等function参数执行完,new Promise才返回一个promise实例对象。
这时候再调用then,其实是已经fullfill了。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
然而,我错了。
感谢 @richard 指正。
因为,如果promise.then注册的是同步回调的话,5应该比3先执行。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
那好吧,从零开始了,重新分析问题。
老老实实翻开规范《ECMAScript® 2015 Language Specification》,搜setTimeout。
纳尼?没有。
想起来了,这是window对象的东西,跟浏览器宿主环境有关,不属于EcmaScript范围。
那怎么搞,每个浏览器难道自己随便实现吗?谷歌了一把,找到了这个:
javascript - setTimeout(): If not defined in EcmaScript spec, where can I learn how it works?
原来HTML5规范中还是有介绍的。6.4 Timers
查了查setTimeout,似乎没什么收获,那再看看HTML的事件循环吧。
一个浏览器环境,只能有一个事件循环
而一个事件循环可以多个任务队列
关于任务源:
它指出,一个浏览器环境(unit of related similar-origin browsing contexts.)只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。
相同任务源的任务,只能放到一个任务队列中。
不同任务源的任务,可以放到不同任务队列中。
(同一个任务队列,能否容纳不同任务源的任务,没说)
又举了一个例子说,客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,而且别的任务也能执行到了。但是,同一个任务队列中的任务必须按先进先出的顺序执行。
结论:可以有多个任务队列,目的想必是方便调整优先级吧。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
回到EcmaScript规范上来,
76页 8.4 Jobs and Job Queues
<img src="https://pic4.zhimg.com/5014ab6454bc214f76e3260fb68c3a1b_b.png" data-rawwidth="660" data-rawheight="178" class="content_image" width="660" data-original="https://pic4.zhimg.com/5014ab6454bc214f76e3260fb68c3a1b_r.png">它指出,任务队列(Job queue)是一个先进先出的队列,每一个任务队列是有名字的,至于有多少个任务队列,取决于实现。每一个实现至少应该包含以上两个任务队列。
它指出,任务队列(Job queue)是一个先进先出的队列,每一个任务队列是有名字的,至于有多少个任务队列,取决于实现。每一个实现至少应该包含以上两个任务队列。
以下又强调了,单独的任务队列中的任务总是按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行。
哪里用到这个任务队列了呢,Promise就用了,492页。
<img src="https://pic4.zhimg.com/fed4b5f8710e3c3473e12a181394845f_b.png" data-rawwidth="657" data-rawheight="381" class="content_image" width="657" data-original="https://pic4.zhimg.com/fed4b5f8710e3c3473e12a181394845f_r.png">题主的问题,属于fulfilled的情况,如图所示。
题主的问题,属于fulfilled的情况,如图所示。
会把一个任务放到名为“PromiseJobs”的任务队列中。
结论:EcmaScript的Job queue与HTML的Task queue有异曲同工之妙。它们都可以有好几个,多个任务队列之间的顺序都是不保证的。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
那为什么setTimeout会后执行呢,可能是它所属的任务队列优先级比较低吧。
我之前对这个问题产生了误解,主要是规范研究的不仔细,以为任务队列只有一个。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
更新一:
关于和tick的关系,群里的小伙伴们尝试了各种办法,在昵称为“第七片魂器”大神的指点下,
我们先后用setImmediate和process.nextTick进行了实验。(node.js。。。
setImmediate(function(){ console.log(1); },0); setTimeout(function(){ console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); }); console.log(6); process.nextTick(function(){ console.log(7); }); console.log(8);
结果:3 4 6 8 7 5 2 1
事件的注册顺序如下:
setImmediate - setTimeout - promise.then - process.nextTick
因此,我们得到了优先级关系如下:
process.nextTick > promise.then > setTimeout > setImmediate
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
更新二:
后来讨论进入了白热化,大神“東方(成都-潜水猪)”提到了promsieA+规范。
然而,后面的译者注有问题,与前面的优先级实验不符。
process.nextTick > promise.then > setTimeout > setImmediate
<img src="https://pic3.zhimg.com/51f4d8ee42821c681331faf2a09c857e_b.png" data-rawwidth="602" data-rawheight="258" class="content_image" width="602" data-original="https://pic3.zhimg.com/51f4d8ee42821c681331faf2a09c857e_r.png">又翻到了原版:
又翻到了原版:https://promisesaplus.com/
<img src="https://pic2.zhimg.com/1f5d7499d36b96ce51b7e20896acb7c1_b.png" data-rawwidth="803" data-rawheight="213" class="content_image" width="803" data-original="https://pic2.zhimg.com/1f5d7499d36b96ce51b7e20896acb7c1_r.png">原版的含义,有种依赖于实现的意思,随便玩。。。
原版的含义,有种依赖于实现的意思,随便玩。。。
而且macro-task和micro-task到底包含哪些也没详细说。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
更新三:
然后,重新看了遍聊天记录,原来“-超亼/夿夿(广州—坚壳)”大神已经指出过了。。。
又翻到了汉语版promsieA+规范中引的那篇文章。
文章链接:Promise进阶介绍+原生实现
一切都明朗了。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
更新四:
其实大神“最爱柠檬(南京-菜B檬檬)”一开始就贴了V8源码。。。
只是吾等平民,实在太锉了。。。
(眼下,正在被虐中。。。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
感悟1:人丑就该多读书。
感悟2:你永远都没有新的想法,除非在写博士论文。
感悟3:我不生产答案,我只是大神们聊天记录的搬运工。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(先发这些,以后有新结论再更新。。。
剩下的疑问:
1. process.nextTick也会放入microtask quque,为什么优先级比promise.then高呢?
2. 到底setTimeout有没有一个依赖实现的最小延迟?4ms?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2017.02.25更新
剩下的疑问,第一个问题,已经被评论区,大神 @黄一君 指出,以下是原文,
“process.nextTick 永远大于 promise.then,原因其实很简单。。。在Node中,_tickCallback在每一次执行完TaskQueue中的一个任务后被调用,而这个_tickCallback中实质上干了两件事:
1. nextTickQueue中所有任务执行掉(长度最大1e4,Node版本v6.9.1)
2.第一步执行完后执行_runMicrotasks函数,执行microtask中的部分(promise.then注册的回调)
所以很明显process.nextTick > promise.then”
第二个问题,也由评论区,大神 @鲁小夫 指出,
“4ms已经标准化了”
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
2017.03.08更新
上文为了强调任务队列可以有多个是这样说的:
“有一个事件循环,但是任务队列可以有多个。”
感谢知友 @Jschyz 指出错误:
> 根据2017年新版的HTML规范,浏览器包含2类事件循环:browsing contexts 和 web workers。 链接:HTML Standard
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
> Today is better than yersterday.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
QQ群:JS高级前端开发 159758989
总入口:JS前端开发跳板群 492107297
这个没啥纠结的,因为 setTimeout 是属于 macrotask 的,而整个 script 也是属于一个 macrotask, promise.then 回调 是 microtask ,执行过程大概如下:
- 由于整个 script 也属于一个 macrotask, 由于会先执行 macrotask 中的第一个任务,再加上promise 构造函数因为是同步的,所以会先打印出 1和2
- 然后继续同步执行末尾的 console.log(3) 打印出3
- 此时 setTimeout 被推进到 macrotask 队列中, promise.then 回调被推进到 microtask 队列中
- 由于在第一步中已经执行完了第一个 macrotask , 所以接下来会顺序执行所有的 microtask, 也就是 promise.then 的回调函数,从而打印出5
- microtask 队列中的任务已经执行完毕,继续执行剩下的 macrotask 队列中的任务,也就是 setTimeout, 所以打印出 4