Js 详解事件循环 Event Loop,以及宏任务和微任务

66 阅读2分钟

\

首先要明白事件循环需要先了解一下[渲染进程],也就是浏览器内核

我们知道一个进程内包含着多个线程,因此渲染进程它也是一个多线程程序,它其中包括着五个线程。

  1. js引擎线程
  2. 事件触发线程
  3. 定时任务线程,该线程是浏览器专门提供用来管理定时任务
  4. 异步请求线程,该线程是浏览器专门提供用来管理异步请求任务的,例如ajax
  5. gui渲染线程,负责页面渲染的事务

首先呢 js中包含了同步任务和异步任务两种,

  1. 首先js引擎线程会创建一个执行栈,所有的同步任务都会被放到这个执行栈中来执行
  2. 同时事件触发线程管理着一个任务队列,当一个异步任务被触发后它内部的回调函数就会被放到这个任务队列中,注意此时它并不会执行这个回调函数,只是将它放入任务队列。
  3. 然后等js引擎线程中的执行栈中的同步任务执行完成之后,它就会去读取事件触发线程所管理的任务队列,将任务队列中的回调函数放入到执行栈中,然后重新执行。此时就完成了一个基本的事件循环

(上图就是一个事件循环的大致过程)

这里多说几句 ,setTimeout和setInterval可以用来创建定时任务,xmlHttpRequest和fetch可以用来发起一个异步请求任务,但我们要明白的是,这些任务本身他们是同步执行,他们本身是同步任务,只是他们内部的回调函数是异步的

比如,当代码执行到xmlHttpRequest时,实际上是【js引擎线程】通知【异步请求线程】去发送一个异步请求,当这个请求完成后去执行一个回调函数,而当【异步任务线程】接收这个通知之后,会在异步任务完成后将异步任务中的回调函数放到【事件触发线程】中的任务队列中。

而当代码执行到setTimeout和setInterval时,事实上是【js引擎线程】去通知【定时任务线程】 ,每隔一个时间段去执行一个回调函数,而当【定时任务线程】接收到这个通知之后,就会等时间间隔到了之后,将这个回调函数放到【事件触发线程】所管理的任务队列中。

然后等js引擎线程将执行栈的同步任务执行完成之后就会去读取事件触发线程的任务队列,将队列中的回调函数放到执行栈中重新执行,从而完成一个完整的事件循环

有些时候我们经常会遇见类似这样的面试题,答案我们知道 先输出 5 然后输出 1,这内部其实就事件循环所导致的,

setTimeout(()=>{
	console.log(1)
},0)
	
		
console.log(5)

因为外面那个console.log(5) 是个同步任务 它会先在执行栈中执行,等执行完成之后执行栈处于空闲状态了,系统就会去读取事件触发线程的任务队列中的回调函数,也就是定时器中的那个,因此1反而会后执行。

也就是说并不是定时器时间间隔为0它就会立即执行,它任然需要等待执行栈中的任务执行完成之后才会轮到它。

用几句话简单概括一下事件循环

  1. js引擎线程只会去执行执行栈中的任务
  2. 当执行栈中的任务全部执行完成后它就会去读取事件触发线程的任务队列中的回调函数
  3. 而事件触发线程的任务队列中的回调函数又是各自线程中的异步任务插入进来的
  4. 从而循环往复完成一个事件循环

宏任务和微任务


首先 在执行栈中执行的所有代码都是宏任务,包括它从事件队列中读取的也是。

比如 setTimeout和setInterval 它们就是宏任务。

我们之前说过渲染进程里面包含着一个gui渲染线程和js引擎线程。

这个js引擎线程它是一个单线程,它负责执行js代码,而gui渲染线程它负责渲染页面,只要是和页面渲染相关的都归他管。

但是,这个两个线程他们是一个互相排斥的关系,就是js引擎线程在执行时渲染线程肯定不会执行,渲染线程执行时js引擎肯定没有在执行,为什么呢,因为我们知道js是可以做dom操作的,而当js更改dom的时候如果正好在渲染页面,就会导致渲染后的结果不可控,因此它们两个处于互斥关系。

1 宏任务
浏览器在执行的时候呢,会先执行宏任务,然后执行完成之后再在下一个宏任务开始之前执行渲染任务,然后渲染任务完成后再执行下一个宏任务,这样循环往复


2 微任务
这里先说一下,promise就是异步微任务,接下来说下微任务是怎么运行的。
我之前说了,宏任务执行完之后会立即执行一个渲染任务,而微任务它又是在渲染任务之前执行。
这是由js异步机制所导致,我们之前说过,在执行异步任务的时候,js会在异步任务触发后将它的回调函数放到事件队列中,这没有问题,但如果这是一个异步微任务的话情况就不同了,异步微任务它也会被放到事件队列中,但是这里有个问题,微任务它放进的队列是【微任务队列】,它和宏任务的事件队列不是同一个队列,而它从事件队列中取出回调函数时是先从【微任务队列】取,在取【宏任务队列】的回调函数
顺序是这样的,

  1. js首先先执行一个宏任务,然后在执行的过程中如果碰见了微任务它就将微任务放到【微任务队列】中,
  2. 然后等宏任务执行完成之后它就会将【微任务队列】中的所有微任务全部执行完,
  3. 然后在执行渲染任务,
  4. 渲染任务执行完成之后再重事件队列中取出下一个宏任务开始执行。

具体如下图

例子1

在改动一下之前的题目,比急着给出答案,想好了再说

setTimeout(()=>{
	console.log(1)
},0)
	
setTimeout(()=>{
	console.log(2)
},0)

new Promise(function(resolve){
	console.log('3');
    resolve();
}).then(function(){
	console.log('4')
});
		
console.log(5)

我知道,有些同学可能第一反应是 3 5 1 2 4

但实际上正确答案是3 5 4 1 2

解析 :首先,先输出3和5这大家应该都没有疑问,具体是后面的顺序,这是因为promise属于异步微任务,而setTimeout属于异步宏任务,微任务会在下一个宏任务开始之前执行。当console.log(3) 和 console.log(5) 这两个宏任务先执行完之后,会立即执行微任务,因此先输出4,而后在执行宏任务,因此之后再输出1和2

例子2

再改动一下上面的题目

setTimeout(()=>{
	console.log(1)

    new Promise(function(resolve){
	    console.log('6');
        resolve();
    }).then(function(){
	    console.log('7')
    });

},0)
	
setTimeout(()=>{
	console.log(2)
},0)

new Promise(function(resolve){
	console.log('3');
    resolve();
}).then(function(){
	console.log('4')
});
		
console.log(5)

正确答案是3 5 4 1 6 7 2

解析:首先经过了列子1后, 对于先输出3 5 4这应该没什么好说的。

首先,当微任务执行完之后,会从事件队列中读取下一个宏任务来执行,也就是下面这个定时器任务

setTimeout(()=>{
	console.log(1)

    new Promise(function(resolve){
	    console.log('6');
        resolve();
    }).then(function(){
	    console.log('7')
    });

},0)

然后它会先输出1和6,由于这个宏任务中它创建了一个promise微任务,因此它会在宏任务 consloe.log(6) 之后立即就执行,因此此时再输出7,然后再执行下一个宏任务最后输出2

以上就是事件循环、宏任务以及微任务的执行过程了