这篇文章主要是看了【前端小黑屋】的文章--从多线程角度来看 Event Loop的学习记录和总结,由于之前为了理解js的事件循环,找过很多相关文章去学习,但印象给我最深刻的、最直观的,就是这篇文章,通过操作dom,结合多线程的关系,去解释js的event loop的过程是怎么样的,特别是通过操作dom的颜色变化,让你很直观地感受到event loop机制的内貌。
进程和线程
计算机里面的一个应用程序,就为一个进程,比如浏览器。但是浏览器是属于多进程应用。什么是多进程应用呢?
多进程,就是说这个程序的进程由多个子进程组成。比如浏览器每打开一个tab页就是一个子进程,比如浏览器的一些扩展插件,又是一个子进程。
进程和线程是一对多的关系,在前端的操作中,最重要的就是浏览器的渲染进程,渲染进程也就是我们所说的内核。在渲染进程当中,有多个线程:
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 定时触发器线程
- 异步http请求线程
其中,GUI渲染线程和JS引擎线程是互斥关系,就是,当他们其中一个线程工作时,另外一个就不工作。还有就是,我们都知道,js是单线程的,那为什么js设计的时候不是多线程的呢?为什么要设计成单线程? js设计成单线程,有两个原因:
- 因为历史原因,在js诞生的时候,多进程多线程的架构并不流行,硬件支持并不好。
- 可以想象一下,如果js是多线程的,如果其中一个线程在操作dom,另外一个线程也在同时操作同一个dom,最后浏览器渲染的结果就不可预期了。 还有GUI渲染线程和JS引擎线程是互斥关系,也是为了让操作dom,不发生矛盾,保持一致性。
Event Loop
首先,需要区分同步任务和异步任务:什么才算同步任务?哪些才算是异步任务?浏览器内核里面的五个线程都干什么用负责啥?哪些归谁管哪些不归谁管?什么才算是宏任务?什么才算微任务?上面逼逼了那么多到底啥是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引擎线程执行。
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线程继续接管,开始下一个宏任务(从事件队列中获取)