javascript事件循环机制
一、前言
首先,我们知道javascript是一门单线程的非阻塞的脚本语言,这是由其最初的用途来决定的:与浏览器交互。
单线程则意味着,javascript代码在执行时,都只有一个主线程来处理所有的任务。而非阻塞则是当代码需要进行一项异步任务时(无法立即返回结果,需要花一定时间才能返回的任务,如I/O事件),主线程会挂起这个任务,然后在异步任务返回结果之后再根据一定规则去执行相应的回调。
单线程的必要性:浏览器是javascript最主要的执行环境,在其中我们需要进行各种各样的dom操作。如果javascript是多线程的,那么当两个线程同时对同一个dom进项操作,比如一个向其添加事件,而另一个则删除了这个dom,此时该如何处理呢?因此,为了保证不发生类似情况,javascript选择只用一个主线程来执行代码,从而保证了程序的一致性。
而另一个特性非阻塞,就是通过事件循环机制实现的(Event Loop)。事件循环机制虽然在浏览器和node中存在相似的部分,但两者间还是有很多的区别,所以分开讨论
二、预备知识
2.1、同步和异步
(1)同步
在函数调用时,调用者能够马上得到结果,那么就是同步的。
(2)异步
无法立即返回结果,需要花一定时间才能返回的任务。常见的异步进程有:
- DOM事件,由浏览器的DOM模块处理,达到触发条件时,在任务队列中添加相应的回调函数
- setTimeout定时器等,由浏览器的timer模块处理,达到设置的时间点时,在任务队列中添加回调
- ajax请求,由浏览器的NetWork模块处理,等待请求返回后,在任务队列中添加回调
2.2、执行栈
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算
由上可知,栈是一种先进后出的数据结构,我们通过运行以下代码,来模拟它的入栈和出栈过程。
function fun2() {
console.log('fun2')
}
function fun1() {
fun2();
}
setTimeout(() => {
console.log('setTimeout')
})
fun1();
2.3、任务队列
以上2.2中展示的是同步代码的执行,当js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
当异步事件返回结果后,js会将这个事件加入不同于当前执行栈的另一个队列,称之为任务队列。事件队列中的任务,需要等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,主线程才会去查找任务队列中是否有任务。如果有,再根据一定规则执行,执行的时候同样也是先执行其中的同步代码,如此反复,就形成了‘事件循环’。
任务队列分为宏任务队列和微任务队列,当异步事件返回结果后,就会根据这个异步事件的类型,放到对应的宏任务队列或者微任务队列中。等主线程闲置当前执行栈为空时,会优先查看微任务队列是否存在任务。如果不存在,再去查看宏任务队列,取出一个事件并把对应的回调加入到当前执行栈;如果存在,则会依次执行微任务队列中事件对应的回调,直到微任务队列为空,再去查看宏任务队列,取出一个事件执行对应回调。如此反复循环。
js中存在多个任务队列,并且不同的任务队列之间优先级不同,优先级高的先被执行,同一队列中按队列顺序被执行。任务队列分两种类型:
- macro task queue(宏任务队列):script(整体代码),setTimeout,setInterval,setImmediate,I/O,页面渲染
- micro task queue(微任务队列):process.nextTick,new Promise,new MutaionObserver()
三、事件循环机制
总结以上:
(1)先按顺序从上到下执行当前全局上下文
(2)遇到异步事件就把它交给对应的浏览器模块
(3)浏览器模块处理完返回结果后,宏任务放入宏任务队列,微任务放入微任务队列
(4)当主进程执行栈任务清空后,开始执行任务队列。先执行微任务队列,执行完微任务队列后再执行宏任务队列
(5)在执行任务队列时,重复以上步骤。如果遇到异步进程,相当于再开了一个宏任务队列和微任务队列,按照如上步骤执行完后再去执行新的微任务队列。一直循环下去,直到任务队列清空。
四、例题
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' )
分析:
(1)首先执行同步代码,console.log( ‘script start’ )
(2)遇到setTimeout,会被推入宏任务队列
(3)执行async1(), 它也是同步的,只是返回值是Promise,在内部首先执行console.log( ‘async1 start’ )
(4)然后执行async2(), 然后会打印console.log( ‘async2’ )
(5)从右到左会执行, 当遇到await的时候,阻塞后面的代码,去外部执行同步代码
(6)进入 new Promise,打印console.log( ‘promise1’ )
(7)将.then放入事件循环的微任务队列
(8)继续执行,打印console.log( ‘script end’ )
(9)外部同步代码执行完毕,接着回到async1()内部, 由于async2()其实是返回一个Promise, await async2()相当于获取它的值,其实就相当于这段代码Promise.resolve(undefined).then((undefined) => {}),所以.then会被推入微任务队列, 所以现在微任务队列会有两个任务。接下来处理微任务队列,打印console.log( ‘promise2’ ),后面一个.then不会有任何打印,但是会
(10)执行后面的代码, 打印console.log( ‘async1 end’ )
(11)进入第二次事件循环,执行宏任务队列, 打印console.log( ‘setTimeout’ )
当我们在函数前使用async的时候,使得该函数返回一个Promise对象。当使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码, 并且当await执行完毕之后,会先处理微任务队列的代码
参考:
https://blog.csdn.net/weixin_39256211/article/details/88855627
https://zhuanlan.zhihu.com/p/33058983 (详解浏览器和node不同环境下js引擎的事件循环机制)
https://blog.csdn.net/Jermyo/article/details/103237796 (例题很多,宏任务有页面渲染的例子)