js 事件循环(event loop)、宏任务、微任务
前言
Google工程师Jake Archibald
写了一篇关于事件循环以及宏任务微任务的文章,非常经典,本文的例子都来源于改文章,有兴趣的可以去浏览原文Tasks, microtasks, queues and schedules
在该文中也提供了Philip Roberts
在youtube上的一个关于事件循环的视频What the heck is the event loop anyway?(需要梯子),视频里讲的非常好,有兴趣可以看下。
事件循环的基本概念
JavaScript
是单线程的,同时只能执行一个代码片段。如果存在耗时的异步操作,则会被阻塞(blocking
).JavaScript
代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue
)来搞定另外一些代码的执行,例如setTimeout
,AJAX(XHR)
等,这些都是Web API
,由浏览器提供,不存在于V8引擎
内。- 任务队列又分为
macro-task
(宏任务)与micro-task
(微任务),在最新标准中,它们被分别称为task
与jobs
。微任务在一次事件循环中,总是先于宏任务执行 - 一个线程中,事件循环是唯一的,但是任务队列里的任务可以拥有多个。
macro-task
大概包括:script
(整体代码),setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。micro-task
大概包括:process.nextTick
,Promise
的then catch finally
,Object.observe
(已废弃),MutationObserver
(html5
新特性)setTimeout/Promise
等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。Promise
的实例化会在主执行栈中执行
通过一个简单的例子来理解
代码
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');
打印结果:
script start
script end
promise1
promise2
setTimeout
为什么会有这样的结果?
逐步分析:
- 主执行栈开始,执行
console.log('script start');
,打印script start
- 执行栈往下执行
setTimeout
,由于setTimeout
是属于宏任务,浏览器等待0ms
,将setTimeout
的callback
函数放入宏任务队列中 - 执行栈继续执行
Promise
,由于Promise
的then
属于微任务,则将第一个then
中的回调函数放入到微任务队列中,第二个then
需要等待第一个then
执行完毕才会执行,因此这里不会讲第二个then
的回调函数放入微任务队列中 - 执行栈执行
console.log('script end');
,打印script end
- 由于微任务总是先于宏任务执行,则先取出微任务队列中的第一个then的回调函数执行,打印
promise1
- 第一个
then
执行完,紧接着又有一个then
,再将其放入微任务队列中 - 只要微任务队列中还有任务没有执行完,则优先执行,因此执行第二个
then
的回调函数,打印promise2
- 微任务队列都执行完毕,此时执行宏任务队列里的任务,打印
setTimeout
动画演示:
一个更复杂的例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.outer {
background: #D4D4D4;
padding: 25px;
width: 92px;
margin: 0 auto;
}
.inner {
background: #ADADAD;
padding: 46px;
width: 0;
}
</style>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
<script>
// 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);
</script>
</body>
</html>
打印结果:
click
promise
mutate
click
promise
mutate
timeout
timeout
我们这里就不逐步分析了,由于事件的冒泡机制,当第一个事件循环结束后,事件冒泡,会开启第二个事件循环,重复来一遍,看下动画演示:
使用代码派发事件会发生什么事?
上面的代码,我们是通过点击鼠标来触发事件,但是如果我们直接在代码中触发事件,会发生什么事呢?
// 其他代码不变
inner.click(); // 在代码中派发事件
打印结果:
click
click
promise
mutate
promise
timeout
timeout
为什么差距这么大,哪里影响了代码的执行顺序?实际上是我们触发事件的方式引起的区别(废话),当直接在代码中触发事件的时候,此时js主执行栈并不是空的,因此不会先去执行微任务宏任务队列中的任务,再去触发outer element
的点击事件,而是直接触发outer element
的点击事件,当主执行栈没有任务的时候,再去执行微任务宏任务队列中的任务。
为什么mutate
只打印了一次,那是因为MutationObserver
的监听是异步触发,在所有的DOM操作完成后才触发使回调函数进入微任务队列。
比如,程序中有10个修改DOM的操作,只有在第十个处理完之后,回调函数才进入微任务队列。
通过动画就一目了然了:
async/await的事件循环
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
console.log( 'script start' )
setTimeout( function () {
console.log( 'setTimeout' )
}, 0 )
async1();
new Promise(function ( resolve ) {
console.log( 'promise1' )
resolve();
}).then(function () {
console.log( 'promise2' )
})
console.log( 'script end' )
执行结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
await
之后的代码必须等await
语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完await
语句,才把await
语句后面的全部代码加入到微任务行列
await
语句是同步的,await
语句后面全部代码才是异步的微任务
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com