JavaScript中的事件循环机制
最近看面经,发现很多家公司都问事件循环机制(Event Loop),但是在网页上找,没发现能让我看明白的,于是找了个时间研究了一下,下面内容都是我自己的理解,不喜勿喷。
一.事件循环有什么用?
事件循环(Event Loop)是一种计算机系统的运行机制,JavaScript就采用的这种机制来解决浏览器单线程的问题,用于等待和发送消息和事件。
二.学习事件循环之前要了解的概念:同步任务、异步任务、宏任务、微任务
JavaScript单线程任务从时间上分成同步任务和异步任务,而异步任务又分成宏任务(macro Task)和微任务(micro Task)
对于同步和异步的理解:
JavaScript是一门“单线程”的语言,可以理解为,在我们执行程序中的代码时,只有一条路径可以输出,因此把一个线程比作成一条流水线,同步和异步的差别就在于这条流水线上各个流程的执行顺序不同。
同步:要求每一条程序必须按照顺序执行,如果前面的程序没有执行完成,那么后面的程序也不能执行,这样就会产生执行程序浪费大量的时间。
异步:可以简单理解为可以改变程序正常执行顺序的操作就可以看成是异步操作,即后面的程序不需要等待前面的程序执行完成就可以执行操作。
对于宏任务和微任务的理解:
宏任务:setTimeOut、setInterval、setImmediate、requestAnimationFrame、I/O操作、UI渲染
微任务:promise、async/await(返回的也是一个promise)、process.nextTick、MutationObserver。
三:事件循环的实现
在JavaScript中,事件循环机制的实现主要由三个部分组成:调用栈(call stack),消息队列(Message Queue),微任务队列(Microtask Queue)
在事件循环(Event Loop)开始时,会从全局栈一行一行执行,遇到函数调用,会将该函数压入调用栈中,被压入的函数叫做帧(Frame),当函数返回后,会从调用栈中弹出。
(1).同步任务对事件循环的实现
例如:
function func1(){ console.log(1); } function func2(){ console.log(2); func1(); console.log(3); } func2();
关系图如下图所示:
执行过程:
(1).把func2();压入调用栈中,执行它里面的代码。
(2).遇到console.log(2);把它压入栈中,并执行打印出2,之后弹出。
(3).接下来,调用func1();使其被压入栈中,执行它里面的代码。
(4).console.log(1)被压入栈中,并执行打印出1,之后弹出,func1();执行完毕,弹出。
(5).console.log(3)压入栈中,执行并打印出3,之后弹出,func2();执行完毕,弹出。
到这里,整个调用栈被清空。
输出:
(2).异步任务对事件循环的实现:宏任务
对JavaScript中宏任务的异步操作,例如:setTimeout()等函数,会入队到消息队列中,成为消息。
例如:
function func1(){ console.log(1); } function func2(){ setTimeout(()=>{ console.log(2); },0) func1(); console.log(3); } func2();
关系如图所示:
执行过程:
(1).把func2();压入调用栈中,执行它里面的代码。
(2).当setTimeout()被压入栈时,它里面的回调函数会入队到消息队列中,消息会在调用栈清空的时候执行。
(3).接下来,调用func1();使其被压入栈中,执行它里面的代码。
(4).console.log(1)被压入栈中,并执行打印出1,之后弹出,func1();执行完毕,弹出。
(5).console.log(3)压入栈中,执行并打印出3,之后弹出。
(6).消息队列中的消息会被压入到调用栈中并执行,最后打印出2,之后弹出。
输出:
(3).异步任务对事件循环的实现:微任务
例如:
var p = new Promise(resolve=>{ console.log(4); resolve(5); }) function func1(){ console.log(1); } function func2(){ setTimeout(()=>{ console.log(2); }) func1(); console.log(3); p.then(resolved=>{ console.log(resolved); }) .then(()=>{ console.log(6); }) } func2();
执行过程:
例如:promise()等函数,会加入到微任务队列中。会在调用栈被清空的时候立即执行,并且处理期间,新加入的微任务也会立即执行。例如在代码中加入promise()定义,promise钩子函数首先被压入调用栈,之后console.log(4)和resolve(5)分别压入栈中并执行,最后弹出promise()钩子函数,上面的执行过程和之前的一样。调用func2(),setTimeout()中的匿名回调入队到消息队列中,调用func1()打印出1,然后打印3,之后promise后面两个then的回调函数会入队到微任务队列中,此时调用栈为空,所以执行微任务队列中的任务,把它们压入到调用栈中并执行,打印出5,6,最后压入并执行消息队列中的消息,打印出2,执行结束。
输出: