JS的Event Loop 和 microTask

面试和笔试题目中,常常会出现'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

js异步实现原理

咱们多多少少都应该据说过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

  • Tasks: Run script
  • JS stack: script

二、 遇到setTimeout log: script start

  • Tasks: Run script | setTimeout callback
  • JS stack: script

三、 遇到Promise

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: script

四、 执行最后一行 log: script start | script end

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: script

四、 同步任务执行完毕,弹出相应的stack log: script start | script end

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack:

五、 同步任务最后是microTasks,JS stack压入callback log: script start | script end | promise1

  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then | promise then
  • JS stack: promise1 calback 六、 promise返回新的promise,压入microTasks,继续执行 log: script start | script end | promise1 | promise2
  • Tasks: Run script | setTimeout callback
  • Microtasks: promise then
  • JS stack: promise2 calback

八、 第一个Tasks结束,弹出 log: script start | script end | promise1 | promise2

  • Tasks: setTimeout callback
  • Microtasks:
  • JS stack:

九、 下一个Tasks log: script start | script end | promise1 | promise2 | setTimeout

  • Tasks: setTimeout callback
  • Microtasks:
  • JS stack: setTimeout callback

好了,结束了,这就比以前的理解"promise比setTimeout快,异步先执行promise,再执行setTimeout"就深入的多。 由于promise所创建的回掉函数是压入了mircroTasks队列中,它仍然属于当前的Task,而setTimeout则是至关于在Task序列中添加了新的任务

一个更复杂的例子

好了,有了刚才的认识和铺垫,接下来经过一个更加复杂的例子来熟悉JS事件处理的一个过程。

如今有这样一个页面结构:

<div class="outer">
  <div class="inner"></div>
</div>

复制代码

js代码以下,如今若是点击里面的方块,控制台会输出什么呢? 在线实例

// 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之中。 所以

一、点击事件触发

  • Tasks: Dispatch click
  • Microtasks:
  • JS stack:

二、触发点击事件的函数,函数执行,压入JS stack

  • Tasks: Dispatch click
  • Microtasks:
  • JS stack: onClick
  • Log: 'click'

三、遇到setTimeout,压入Tasks队列

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks:
  • JS stack: onClick
  • Log: 'click'

四、遇到promise,压入Microtasks

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then
  • JS stack: onClick
  • Log: 'click'

五、遇到 outer.setAttribute,触发mutation

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack: onClick
  • Log: 'click'

六、onclick函数执行完毕,出JS stack

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack:
  • Log: 'click'

七、这个时候,JS stack为空,执行Microtasks

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Promise.then | Mutation observers
  • JS stack: PromiseCallback
  • Log: 'click' 'promise'

八、microtasks顺序执行

  • Tasks: Dispatch click | setTimeout callBack
  • Microtasks: Mutation observers
  • JS stack: Mutation callback
  • Log: 'click' 'promise' 'mutate'

接下来是重点,当microtasks为空,该执行下一个Tasks(setTimeout)了吗?并无,由于js事件流中的冒泡被触发,也就是在外面的一层Div也会触发click函数,所以咱们把刚才的步骤再走一遍。

过程省略,结果为 九、冒泡走一遍的结果为

  • Tasks: Dispatch click | setTimeout callBack | setTmeout callback(outer)
  • Microtasks: Mutation observers
  • JS stack: Mutation callback
  • Log: click promise mutate click promise mutate

十、 第一个Tasks完成,出栈

  • Tasks: setTimeout callBack | setTmeout callback(outer)
  • Microtasks:
  • JS stack: setTimeout callback
  • Log: click promise mutate click promise mutate timeout

十一、 第二个Tasks完成,出栈

  • Tasks: setTmeout callback(outer)
  • Microtasks:
  • JS stack: setTimeout(outer) callback
  • Log: 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)一样的代码,得出的结果是不一样的哦。关键在于,对与 jobmicroTasks之间的一个联系是很模糊的。 可是咱们就按照Chrome的实现来理解吧。

最后一关

仍是刚才那道题,只不过,我不用鼠标点击了,而是直接执行函数

inner.click()
复制代码

若是这样,结果会同样吗?

答案是:

click
click
promise
mutate
promise
timeout 
timeout
复制代码

What!!??我怎么感受我白学了? 不着急,看下此次的过程是这样的,首先最大的不一样在于,咱们在函数最底部加了一个执行inner.click(),这样子,这个函数执行的过程,都是同步序列里的,因此此次的task的起点就在Run scripts:

一、不一样与鼠标点击,咱们执行函数后,进入函数内部执行

  • Tasks: Run scripts
  • Microtasks:
  • JS stack: script | onClick
  • Log: click

二、遇到setTimeout和promise&mutation

  • Tasks: Run scripts | setTimeout callback
  • Microtasks: Promise.then | Mutation Observers
  • JS stack: script | onClick
  • Log: click

三、接下来关键,冒泡的时候,由于咱们并无执行完当前的script,还在inner.click()这个函数执行之中,所以当onclick结束,开始冒泡时,script并无结束

  • Tasks: Run scripts | setTimeout callback
  • Microtasks: Promise.then | Mutation Observers
  • JS stack: script | onClick(这是冒泡的click,第一次click已经结束)
  • Log: click click

四、冒泡阶段重复以前内容

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Promise.then | Mutation Observers |promise.then
  • JS stack: script | onClick(这是冒泡的click,第一次click已经结束)
  • Log: click click

注意第二次没有增长mutation,由于已经有一个在渲染的了

五、inner.click()执行完毕,执行Microtasks

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Promise.then | Mutation Observers |promise.then
  • JS stack:
  • Log: click click promise

六、按理论执行

  • Tasks: Run scripts | setTimeout callback |setTimeout callback(outer)
  • Microtasks: Mutation Observers |promise.then
  • JS stack:
  • Log: click click promise mutate....

后面的就不解释了,Microtasks依次出栈,接着Tasks顺序执行。

总结

Jake老师的文章,对这个的解析和深刻实在使人佩服,我也在面试中因把event loop解释的较为详尽而被面试官确定,因此若是对异步以及event loop有疑惑的,能够好好的消化下这个内容,一块儿进步!

相关文章
相关标签/搜索