Promise的队列与setTimeout的队列有何关联?

setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ co…
关注者
832
被浏览
82,721

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的事件循环吧。

6.1.4 Event loops


一个浏览器环境,只能有一个事件循环


而一个事件循环可以多个任务队列


关于任务源:


它指出,一个浏览器环境(unit of related similar-origin browsing contexts.)只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。

相同任务源的任务,只能放到一个任务队列中。

不同任务源的任务,可以放到不同任务队列中。

(同一个任务队列,能否容纳不同任务源的任务,没说)

又举了一个例子说,客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,而且别的任务也能执行到了。但是,同一个任务队列中的任务必须按先进先出的顺序执行。


结论:可以有多个任务队列,目的想必是方便调整优先级吧。


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


回到EcmaScript规范上来,

76页 8.4 Jobs and Job Queues

<img src="pic4.zhimg.com/5014ab64" data-rawwidth="660" data-rawheight="178" class="content_image" width="660" data-original="pic4.zhimg.com/5014ab64">它指出,任务队列(Job queue)是一个先进先出的队列,每一个任务队列是有名字的,至于有多少个任务队列,取决于实现。每一个实现至少应该包含以上两个任务队列。

它指出,任务队列(Job queue)是一个先进先出的队列,每一个任务队列是有名字的,至于有多少个任务队列,取决于实现。每一个实现至少应该包含以上两个任务队列。


以下又强调了,单独的任务队列中的任务总是按先进先出的顺序执行,但是不保证多个任务队列中的任务优先级,具体实现可能会交叉执行


哪里用到这个任务队列了呢,Promise就用了,492页。

<img src="pic4.zhimg.com/fed4b5f8" data-rawwidth="657" data-rawheight="381" class="content_image" width="657" data-original="pic4.zhimg.com/fed4b5f8">题主的问题,属于fulfilled的情况,如图所示。

题主的问题,属于fulfilled的情况,如图所示。

会把一个任务放到名为“PromiseJobs”的任务队列中。


结论:EcmaScript的Job queue与HTML的Task queue有异曲同工之妙。它们都可以有好几个,多个任务队列之间的顺序都是不保证的。


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


那为什么setTimeout会后执行呢,可能是它所属的任务队列优先级比较低吧。

我之前对这个问题产生了误解,主要是规范研究的不仔细,以为任务队列只有一个。


- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


更新一:


关于和tick的关系,群里的小伙伴们尝试了各种办法,在昵称为“第七片魂器”大神的指点下,

我们先后用setImmediateprocess.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+规范


图灵社区 : 阅读 : 【翻译】Promises/A+规范


然而,后面的译者注有问题,与前面的优先级实验不符。

process.nextTick > promise.then > setTimeout > setImmediate

<img src="pic3.zhimg.com/51f4d8ee" data-rawwidth="602" data-rawheight="258" class="content_image" width="602" data-original="pic3.zhimg.com/51f4d8ee">又翻到了原版:

又翻到了原版:promisesaplus.com/


<img src="pic2.zhimg.com/1f5d7499" data-rawwidth="803" data-rawheight="213" class="content_image" width="803" data-original="pic2.zhimg.com/1f5d7499">原版的含义,有种依赖于实现的意思,随便玩。。。

原版的含义,有种依赖于实现的意思,随便玩。。。

而且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