搞懂JS的事件循环(Event Loop)和宏任务/微任务

共 5114字,需浏览 11分钟

 ·

2021-05-06 17:48

来源 | https://www.cnblogs.com/EaVango/archive/2021/04/30/14722428.html

事件循环与消息队列

首先大家都知道js是一门单线程的语言,所有的任务都是在一个线程上完成的。而我们知道,有一些像I/O,网络请求等等的操作可能会特别耗时,如果程序使用"同步模式"等到任务返回再继续执行,就会使得整个任务的执行特别缓慢,运行过程大部分事件都在等待耗时操作的完成,效率特别低。

为了解决这个问题,于是就有了事件循环(Event Loop)这样的概念,简单来说就是在程序本身运行的主线程会形成一个"执行栈",除此之外,设立一个"任务队列",每当有异步任务完成之后,就会在"任务队列"中放置一个事件,当"执行栈"所有的任务都完成之后,会去"任务队列"中看有没有事件,有的话就放到"执行栈"中执行。

这个过程会不断重复,这种机制就被称为事件循环(Event Loop)机制。

宏任务/微任务

宏任务可以被理解为每次"执行栈"中所执行的代码,而浏览器会在每次宏任务执行结束后,在下一个宏任务执行开始前,对页面进行渲染,而宏任务包括:

  • script(整体代码)

  • setTimeout

  • setInterval

  • I/O

  • UI交互事件

  • postMessage

  • MessageChannel

  • setImmediate

  • UI rendering

微任务,可以理解是在当前"执行栈"中的任务执行结束后立即执行的任务。而且早于页面渲染和取任务队列中的任务。宏任务包括:

  • Promise.then

  • Object.observe

  • MutaionObserver

  • process.nextTick

他们的运行机制是这样的:

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,js线程继续接管,开始下一个宏任务(从事件队列中获取)

在了解了宏任务和微任务之后,整个Event Loop的流程图就可以用下面的流程图来概括:


例子

如无特殊说明,我们用setTimeout来模拟异步任务,用Promise来模拟微任务。

主线程上有宏任务和微任务

console.log('task start');
setTimeout(()=>{ console.log('setTimeout')},0)
new Promise((resolve, reject)=>{ console.log('new Promise') resolve()}).then(()=>{ console.log('Promise.then')})
console.log('task end');
//----------------------执行结果----------------------// task start// new Promise// task end// Promise.then// setTimeout

这个例子比较简单,就是在主任务上加了一个宏任务(setTimeout),加了一个微任务(Promise.then),看执行的顺序,打印出了主任务的task start、new Promise、task end,主任务完成,接下来执行了微任务的Promise.then,到此第一轮事件循环结束,去任务队列里取出了setTimeout并执行。

在微任务中添加宏任务和微任务

跟上个例子相比,我们在Promise.then里加上一个setTimeout和一个Promise.then。

console.log('task start');
setTimeout(()=>{ console.log('setTimeout1')},0)
new Promise((resolve, reject)=>{ console.log('new Promise1') resolve()}).then(()=>{ console.log('Promise.then1') setTimeout(()=>{ console.log('setTimeout2') },0) new Promise((resolve, reject)=>{ console.log('new Promise2') resolve() }).then(()=>{ console.log('Promise.then2') })})
console.log('task end');
//----------------------执行结果----------------------// task start// new Promise1// task end// Promise.then1// new Promise2// Promise.then2// setTimeout1// setTimeout2

猜对了么,正常的主任务没有变化,只是在执行第一次微任务的时候,发现了一个宏任务,于是被加进了任务对了。

遇到了一个微任务,放到了微任务队列,执行完之后又扫了一遍微任务队列,发现有微任务,于是接着执行完微任务,到这,第一遍事件循环才结束,从任务队列里拿出了两次setTimeout执行了。

在异步宏任务中添加宏任务和微任务

其他无异,把刚才添加到Promise.then中的内容添加到setTimeout中。

console.log('task start')
setTimeout(()=>{ console.log('setTimeout1') setTimeout(()=>{ console.log('setTimeout2') },0) new Promise((resolve, reject)=>{ console.log('new Promise2') resolve() }).then(()=>{ console.log('Promise.then2') })},0)
new Promise((resolve, reject)=>{ console.log('new Promise1') resolve()}).then(()=>{ console.log('Promise.then1')})
console.log('task end')
//----------------------执行结果----------------------// task start// new Promise1// task end// Promise.then1// setTimeout1// new Promise2// Promise.then2// setTimeout2

第一遍主任务执行大家都很明白了,到Promise.then1结束,然后取任务队列中的setTimeout,执行过程中又发现了一个setTimeout,放到任务队列中,并且发现一个Promise.then2,把这个微任务执行完之后,第二遍事件循环才结束,然后开始第三遍,打印出了setTimeout2。

加入事件冒泡

事件循环遇到事件冒泡会发生什么?

<div class="outer">  <div class="inner"></div></div>
var outer = document.querySelector('.outer');var inner = document.querySelector('.inner');
function onClick() { console.log('click'); setTimeout(function() { console.log('setTimeout'); }, 0);
Promise.resolve().then(function() { console.log('new Promise'); });}
inner.addEventListener('click', onClick);outer.addEventListener('click', onClick);

点击inner,结果:

click    //inner的clickpromise    //inner的promiseclick    //outer的clickpromise    //outer的promisetimeout    //inner的timeouttimeout    //outer的timeout

我觉得解释应该是这样的:
1、开始执行,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入任务队列。接着执行,打印了click,把timeout放入任务队列,把promise放入了微任务队列。

2、执行栈清空,check微任务队列,发现微任务,打印promise,第一遍事件循环结束。

3、从任务队列里取出任务,执行outer的click事件,打印click,把outer的timeout放入任务队列,把outer的promise放入了微任务队列。执行inner放入任务队列的timeout。

4、执行栈清空,check微任务队列,发现微任务,打印promise,第二遍事件循环结束。

5、从任务队列里取出任务,把timeout打印出来。

JS触发上面的click事件

一样的代码,只不过用JS触发结果就会不一样。
对代码做了稍稍改变,将click拆分成两个方法,方便追踪是谁被触发了。

var outer = document.querySelector('.outer');var inner = document.querySelector('.inner');
const onInnerClick = (e) => { console.log('inner cilcked');
setTimeout(function() { console.log('inner timeout'); }, 0);
Promise.resolve().then(function() { console.log('inner promise'); });}
const onOuterClick = (e) => { console.log('outer clicked');
setTimeout(function() { console.log('outer timeout'); }, 0);
Promise.resolve().then(function() { console.log('outer promise'); });}
inner.addEventListener('click', onInnerClick);outer.addEventListener('click', onOuterClick);
inner.click();

执行结果:

inner cilckedouter clickedinner promiseouter promiseinner timeoutouter timeout

之所以会出现这样的差异,我的理解是JS代码执行中的click事件,分发了一个同步的冒泡事件。所以在第一个click事件结束之后,调用栈中有outer的click事件,所以出现了两个连续的click。

这也是根据结果猜测过程,心里没底。

参考资料:
什么是 Event Loop?

Tasks, microtasks, queues and schedules
js中的宏任务与微任务

学习更多技能

请点击下方公众号

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报