面试和笔试题目中,常常会出现'promise','setTimeout'等函数混合出现时候的运行顺序问题。 咱们都知道这些异步的方法会在当前任务执行结束以后调用,但为何'promise'会在'setTimeout'以前执行? 具体的实现原理是什么?javascript
有和我同样正在为秋招offer奋斗的小伙伴,欢迎到github获取更多个人总结和踩过的坑,一块儿进步→→→→传送门html
上面问题的答案,都在文章《Tasks, microtasks, queues and schedules》讲的很是透彻。 建议英文能够的同窗直接看这篇文章,就不要看我这个“笔记”了。( 之因此叫笔记,由于大部份内容出自文章,可是又不是按字翻译 )前端
如下的题目是咱们刷题能够常常看到的一个常规题目:html5
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
复制代码
几乎每一个前端er均可以坚决果断的给出答案:java
script start
script end
promise1
promise2
setTimeout
复制代码
问题来了,为何promise
的异步执行会在setTimeout
以前,甚至setTimeout
设置的延时是0都不行。 还有在Vue中,咱们经常使用的nextTick()函数原理中,说的microtasks是什么东西? 一切的解释都在开头给的文章中。git
ps: 再次再次声明,这篇文章仍然是我记得笔记,原文比我写的好得多,英文能够的小伙伴强烈推荐看原文。github
咱们多多少少都应该据说过event loop,js是单线程的,经过异步它变得很是强大,而实现异步主要就是经过将异步的内容压入tasks,当前任务执行结束以后,再执行tasks中的callback。面试
Tasks,是一个任务队列,Js在执行同步任务的时候,只要遇到了异步执行和函数,都会把这个内容压入Tasks中,而后在当前同步任务完成后,再去Tasks中执行相应的回调。 举个例子,好比刚才代码中的setTimeout
,当遇到这个函数,总会跟一个异步执行的任务(callback),那么这个时候,Tasks队列里,除了当前正在执行的script以外,会在后面压入一个setTimeout callback
, 而这个callback的调用时机,就是在当前同步任务完成以后,才会调用。这就是为何,'setTimeout' 会出如今'script end'以后了。chrome
MicroTasks,说一些这个,这个和setTimeout
不一样,由于它是在当前Task完成后,就当即执行的,或者能够理解成,'microTasks老是在当前任务的最后执行'。 另外,还有一个很是重要的特性是: 若是当前JS stack若是为空的时候(好比咱们绑定了click事件后,等待和监听click时间的时候,JS stack就是空的),一会当即执行。 关于这一点,以后有个例子会具体说明,先往下看。promise
那么MicroTasks队列主要是promise和mutation observer 的回掉函数生成
好了,刚才大概说了几个概念,那么一开始的例子,到底发生了什么?
talk is cheap, show me a animation!!
---我本身说的
下面的动画说明对整个过程进行了说明:
一、 程序执行 log: script start
二、 遇到setTimeout log: script start
三、 遇到Promise
四、 执行最后一行 log: script start | script end
四、 同步任务执行完毕,弹出相应的stack log: script start | script end
五、 同步任务最后是microTasks,JS stack压入callback log: script start | script end | promise1
log: script start | script end | promise1 | promise2
八、 第一个Tasks结束,弹出 log: script start | script end | promise1 | promise2
九、 下一个Tasks log: script start | script end | promise1 | promise2 | setTimeout
好了,结束了,这就比以前的理解"promise比setTimeout快,异步先执行promise,再执行setTimeout"就深入的多。 由于promise所创建的回掉函数是压入了mircroTasks
队列中,它仍然属于当前的Task,而setTimeout
则是至关于在Task序列中添加了新的任务
好了,有了刚才的认识和铺垫,接下来经过一个更加复杂的例子来熟悉JS事件处理的一个过程。
如今有这样一个页面结构:
<div class="outer">
<div class="inner"></div>
</div>
复制代码
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
复制代码
这里先把正确答案公布,按照以前的理论,正确答案应该是:
click
promise
mutate
click
promise
mutate
timeout
timeout
复制代码
固然,不一样浏览器,对于event loop的实现会稍有不一样,这个是chrome下打印出来的结果,具体的其余形式仍是推荐你们看原文了。
下面分析下,为何是上面的顺序呢?
按照刚才的结论:
click事件显然是一个Task,Mutation observer和Promise是在microTasks队列中的,而setTimeout会被安排在Tasks之中。 所以
一、点击事件触发
二、触发点击事件的函数,函数执行,压入JS stack
三、遇到setTimeout,压入Tasks队列
四、遇到promise,压入Microtasks
五、遇到 outer.setAttribute,触发mutation
六、onclick函数执行完毕,出JS stack
七、这个时候,JS stack为空,执行Microtasks
八、microtasks顺序执行
接下来是重点,当microtasks为空,该执行下一个Tasks(setTimeout)了吗?并无,由于js事件流中的冒泡被触发,也就是在外面的一层Div也会触发click函数,所以咱们把刚才的步骤再走一遍。
过程省略,结果为 九、冒泡走一遍的结果为
click
promise
mutate
click
promise
mutate
十、 第一个Tasks完成,出栈
click
promise
mutate
click
promise
mutate
timeout
十一、 第二个Tasks完成,出栈
click
promise
mutate
click
promise
mutate
timeout
timeout
结束了
因此这里的重点是什么? 是MicroTasks的执行时机: 见缝插针,它不必定就必须在Tasks的最后,只要JS stack为空,就能够执行 这条规则出处在
If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3
另外一方面,ECMA也对此有过说明
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…
— ECMAScript: Jobs and Job Queues
可是对于其余浏览器(firefox safari ie)一样的代码,得出的结果是不一样的哦。关键在于,对与 job
和microTasks
之间的一个联系是很模糊的。 可是咱们就按照Chrome的实现来理解吧。
仍是刚才那道题,只不过,我不用鼠标点击了,而是直接执行函数
inner.click()
复制代码
若是这样,结果会同样吗?
答案是:
click
click
promise
mutate
promise
timeout
timeout
复制代码
What!!??我怎么感受我白学了? 不着急,看下此次的过程是这样的,首先最大的不一样在于,咱们在函数最底部加了一个执行inner.click()
,这样子,这个函数执行的过程,都是同步序列里的,因此此次的task的起点就在Run scripts:
一、不一样与鼠标点击,咱们执行函数后,进入函数内部执行
click
二、遇到setTimeout和promise&mutation
click
三、接下来关键,冒泡的时候,由于咱们并无执行完当前的script,还在inner.click()
这个函数执行之中,所以当onclick
结束,开始冒泡时,script并无结束
click
click
四、冒泡阶段重复以前内容
click
click
注意第二次没有增长mutation,由于已经有一个在渲染的了
五、inner.click()执行完毕,执行Microtasks
click
click
promise
六、按理论执行
click
click
promise
mutate
....后面的就不解释了,Microtasks依次出栈,接着Tasks顺序执行。
Jake老师的文章,对这个的解析和深刻实在使人佩服,我也在面试中因把event loop解释的较为详尽而被面试官确定,因此若是对异步以及event loop有疑惑的,能够好好的消化下这个内容,一块儿进步!