从多线程角度理解Event Loop【学习记录】

302 阅读8分钟

这篇文章主要是看了【前端小黑屋】的文章--从多线程角度来看 Event Loop的学习记录和总结,由于之前为了理解js的事件循环,找过很多相关文章去学习,但印象给我最深刻的、最直观的,就是这篇文章,通过操作dom,结合多线程的关系,去解释js的event loop的过程是怎么样的,特别是通过操作dom的颜色变化,让你很直观地感受到event loop机制的内貌。

进程和线程

计算机里面的一个应用程序,就为一个进程,比如浏览器。但是浏览器是属于多进程应用。什么是多进程应用呢?
多进程,就是说这个程序的进程由多个子进程组成。比如浏览器每打开一个tab页就是一个子进程,比如浏览器的一些扩展插件,又是一个子进程。
进程和线程是一对多的关系,在前端的操作中,最重要的就是浏览器的渲染进程,渲染进程也就是我们所说的内核。在渲染进程当中,有多个线程:

  • GUI渲染线程
  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程

其中,GUI渲染线程和JS引擎线程是互斥关系,就是,当他们其中一个线程工作时,另外一个就不工作。还有就是,我们都知道,js是单线程的,那为什么js设计的时候不是多线程的呢?为什么要设计成单线程? js设计成单线程,有两个原因:

  1. 因为历史原因,在js诞生的时候,多进程多线程的架构并不流行,硬件支持并不好。
  2. 可以想象一下,如果js是多线程的,如果其中一个线程在操作dom,另外一个线程也在同时操作同一个dom,最后浏览器渲染的结果就不可预期了。 还有GUI渲染线程和JS引擎线程是互斥关系,也是为了让操作dom,不发生矛盾,保持一致性。

Event Loop

640.png 首先,需要区分同步任务和异步任务:什么才算同步任务?哪些才算是异步任务?浏览器内核里面的五个线程都干什么用负责啥?哪些归谁管哪些不归谁管?什么才算是宏任务?什么才算微任务?上面逼逼了那么多到底啥是event loop?这个过程是咋样的?第一步第二步第三步...

回答问题:

什么才算同步任务?哪些才算是异步任务?

同步代码不是说只有console.log()、在全局作用调用函数、if等等我们平常熟知的代码,其实setTimeout/setInterval、XHR/fetch这些,也是同步代码,没错,定时器、发送请求这些也是同步代码,他们是和上面那些console.log()是按照顺序执行的,你说这就扯蛋了吧,其实,真正的异步任务,其实是他们里面的回调函数才是异步任务,当代码按照顺序执行到XHR/fetch、setTimeout/setInterval这些同步代码的时候,这些同步代码就已经开始产生他的作用了,只不过我们看到他们里面的回调函数没执行而已,所以就常常误以为这些代码不是同步代码。
那这些XHR/fetch、setTimeout/setInterval东西,都产生了什么作用呢?当他们被执行到的时候,都去干什么事情了呢?

览器内核里面的五个线程都干什么用负责啥?哪些归谁管哪些不归谁管?

接着上面的问题回答,当执行到XHR/fetch、setTimeout/setInterval这些代码的时候,比如执行到setTimeout的时候,js引擎线程就会立马给 定时触发器线程 这大哥打电话通知,因为setTimeout/setInterval这些代码,就是这大哥负责的。然后接收到js引擎线程通知之后,他立马就会在这规定好的时间内,把setTimeout/setInterval里面的回调函数放进 事件队列 当中。同理,当执行到XHR/fetch这些代码时,js引擎线程 就会立马给 异步http请求线程 这位大哥打电话通知,这大哥接收到js引擎线程的通知后,立马发送网络请求,当请求回来的时候,就把里面的回调函数放进 事件队列
所以,回到问题:

  • JS引擎线程:负责执行js代码,也就是负责执行执行栈中里面的js代码,他也只执行里面的代码
  • 定时触发器线程:负责把setTimeout/setInterval里面的回调函数,按照规定时间放进事件队列
  • 异步http请求线程:负责发送请求,请求回来后把回调放进事件队列 那事件触发线程呢?管理事件队列,当js引擎线程把执行栈里面console.log()、if、XHR/fetch、setTimeout/setInterval的同步代码执行完之后,GUI渲染线程就开始工作,进行页面的渲染,就是把dom这些搞出来,颜色大小啥的给你搞得明明白白,注意这时候因为和js引擎线程是互斥,所以此时js引擎线程是不工作的。完了之后,到js引擎线程工作了,他打电话给 事件触发线程 老弟,事件队列有待执行的回调吗?然后老弟说有,然后把回调放进执行栈给js引擎线程执行。

640 (1).png

let timerCallback = function() {
console.log('wait one second');
};
let httpCallback = function() {
console.log('get server data success');
}


// 同步任务
console.log('hello');
// 同步任务
// 通知定时器线程 1s 后将 timerCallback 交由事件触发线程处理
// 1s 后事件触发线程将 timerCallback 加入到事件队列中
setTimeout(timerCallback,1000);
// 同步任务
// 通知异步http请求线程发送网络请求,请求成功后将 httpCallback 交由事件触发线程处理`
// 请求成功后事件触发线程将 httpCallback 加入到事件队列中
$.get('www.xxxx.com',httpCallback);
// 同步任务
console.log('world')
//..
// 所有同步任务执行完后
// 询问事件触发线程在事件事件队列中是否有需要执行的回调函数
// 如果没有,一直询问,直到有为止
// 如果有,将回调事件加入执行栈中,开始执行回调代码

什么才算是宏任务?什么才算微任务?

每次执行栈执行的代码是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行), 每一个宏任务会从头到尾执行完毕,不会执行其他。主代码块,setTimeout,setInterval等,都属于宏任务。微任务可以理解成在当前 宏任务执行后立即执行的任务。 也就是说,当 宏任务执行完,会在渲染前,将执行期间所产生的所有 微任务都执行完。 Promise,process.nextTick等,属于 微任务

事件循环

js执行代码是这样的,最开始先执行完执行栈上面的代码,执行完之后看有没有微任务,没有微任务,- GUI渲染线程进行dom渲染,然后去询问事件队列,看有没有回调任务,有的话,把他放进执行栈,js引擎线程执行代码,然后看有没有微任务。。。一直循环下去
举例1:

document.body.style = 'background:black';
document.body.style = 'background:red';
document.body.style = 'background:blue';
document.body.style = 'background:grey';

分析: 运行这个代码,会发现,页面直接会变成灰色,说明GUI渲染dom的时候是根据最后一行代码去操作的,而此时整个执行栈的代码只有上面的四行,也就是只有一次宏任务。
过程:js引擎线程执行执行栈上的代码,执行完毕,没有微任务,GUI渲染线程渲染dom,渲染完成,Gui线程停止工作,js引擎线程询问事件触发线程,看事件队列有没有回调任务


举例2:

document.body.style = 'background:blue';
setTimeout(function(){
document.body.style = 'background:black'
},0)
setTimeout(function(){
document.body.style = 'background:red'
},0)

分析: 整体代码进入执行栈,js引擎线程执行,遇到第一个setTimeout时,通知定时触发器线程,在规定的时候后将回调派发在事件队列中,然后遇到第二个setTimeout,同理。因为没有微任务,GUI渲染线程渲染页面,页面变蓝色。渲染完之后,JS引擎线程询问事件队列,发现有回调,然后回调进执行栈执行,页面渲染变黑色,依次类推。
过程:页面变蓝,再变黑,最后变成红色。要注意的时,这个代码里面有两个setTimeout,会算为两次宏任务的。总共有三次宏任务。


举例3:

document.body.style = 'background:blue'
console.log(1);
Promise.resolve().then(()=>{
console.log(2);
document.body.style = 'background:black'
});
console.log(3);
//分析:
//整体代码进执行栈
//遇到promise时,此时then里面的回调才算时异步任务,then里面的异步回调为微任务,等待执行
//整体代码都执行完之后,因为有微任务,微任务在渲染之前执行,所以,此时执行then里面的回调
//完了GUI渲染线程工作,页面变成黑色


//过程:
//页面只进行了一次渲染,没有从蓝变黑,说明整体代码宏任务执行后,就执行了微任务,再进行渲染

总结

  • 执行一个 宏任务(栈中没有就从 事件队列中获取)
  • 执行过程中如果遇到 微任务,就将它添加到 微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前 微任务队列中的所有 微任务(依次执行)
  • 当前 宏任务执行完毕,开始检查渲染,然后 GUI线程接管渲染
  • 渲染完毕后, JS线程继续接管,开始下一个 宏任务(从事件队列中获取)

640 (2).png