前言
Event Loop也就是事件循环,是个老生常谈的问题了,而且都是些概念性的东西,略显枯燥,所以我也一直没有对这块进行过整理,奈何面试官总是喜欢去考这一块的知识,没办法只能硬着头皮进行学习,这篇文章也是对我学习到的一点关于Event Loop的只是进行的总结
JavaScript是一门单线程语言
大家都知道,JS是一门单线程非阻塞的语言,在执行任何任务的时候,都通过一个主线程来进行。试想一种情况: 如果JS是多线程的原因,那么同时有两个线程,一个线程对dom元素添加事件,另一个线程则是删除这个dom,那么这种情况要如何处理呢?
为了避免这样的情况发生,当时的布兰登(JavaScript创造者)只能将其设计成单线程非阻塞的语言,用以确保操作上的统一性。
也正是因为JS的单线程特性,也就是所有操作都进行排队,每次都只能执行一个任务,那么为了保证一些耗时较长的操作不会阻塞后续任务的执行,应该如何做呢? 当然就是使用Event Loop机制了。
但是由于在浏览器中的Event Loop是各大厂商根据规范自行实现的,而node中的Event Loop是根据libuv这个库实现的,所以会有不同,因此要分开进行讨论。
浏览器中的事件循环机制
- 关于调用栈和异步事件处理
-
调用栈
首先我们来看如下代码,最开始声明了两个变量a和b,然后按照顺序分别打印出他们:
image.png
那么JS是如何执行他们的呢?
答案是进行排队,然后按照顺序调用,这个排队的地方就是调用栈,上面代码的调用栈顺序如下:
image.png
执行完毕后,该调用栈被销毁,之后开始下一个调用栈的堆栈和执行,这个过程不断重复就是事件循环。- 异步事件处理
讨论这点的时候我们先抛开Promise
以及async/await
,只从最开始的setTimeOut
(setInterval
与setTimeOut
机制类似,所以不讨论)来看,大家都知道setTimeOut
是浏览器自行实现的用于处理异步操作的方法,用法如下,打印结果是123:
image.png
上面代码的意思是先打印一个1,然后将console.log(3)
放入到下一个事件循环的调用栈的首位,然后打印一个2,到了下一个事件循环期间,再打印一个3,如下图所示:
image.png
在该例子中,最多只是存在两个队列,但是后来ECMAScript设计组推出了Promise
和async/await
,这种情况就产生了变化,因为他引入了一个新的队列。
- 异步事件处理
-
macro task和micro task
macro task一般叫做宏任务,micro task叫做微任务,上面例子讨论的只是在宏任务阶段所发生的事,而在Promise
和async/await
被引入后,也同时引入了微任务的概念。也就是说setTimeOut
和同步的操作是属于宏任务阶段的,而Promise
和async/await
是属于微任务队列的。
那么什么是微任务呢?
微任务实际上就是另外一个队列,这个队列中被放入的操作是在前一个宏任务阶段的调用栈中的操作被执行完毕后就立刻开始执行的,执行的顺序为:
-
宏任务队列 -> 微任务队列 -> 宏任务队列
那么也就是说Promise
和async/await
的操作会在setTimeOut
的操作之前被执行。
来看下面例子:
![](https://img.haomeiwen.com/i3360875/02c573b4939e6df9.png)
可以很清晰的看出来,先是打印1,然后打印Promise中2,最后才是打印setTimeOut中的3,虽然他们的顺序是反过来的,反映到图就是如下:
![](https://img.haomeiwen.com/i3360875/64e8e4459019ae57.png)
node中的事件循环
node中的事件循环是基于libuv库实现的,所以与上面浏览器中的会有所不同,两者不可混为一谈。
并且在node中,存在的处理异步的方法比浏览器要多,除了setTimeOut
和setInterval
以及Promise
和async/await
之外,还存在setImmediate
和process.nexttick
这两种方法,他们的执行时机也有所不同。
首先还是来看看node中事件循环的阶段吧。
-
阶段
在node中,事件循环分为如下几个阶段:
image.png
他们对应的作用如下:
- timers: 执行
setTimeOut
和setInterval
中的操作 - I/O callbacks: 执行系统操作回调事件,例如TCP报错等
- idle, prepare: 该阶段用于内部使用,不用管
- poll: 执行同步代码,注意这是启用事件队列时候首先进入的阶段,而不是timers,node有可能会被阻塞在该阶段
- check: 执行
setImmediate
中的操作 - close callbacks: 执行一些事件关闭的回调操作,例如
socket.on(close, xxx)
的回调
- timers: 执行
-
执行顺序
- 首先进入到poll阶段,查看poll队列中是否有任务,有任务的话就按照先进先出的顺序进行执行,当任务执行完毕,poll队列为空时候又或者是poll计时器到达最大限制时候(这个最大限时不是固定的),就将
setTimeOut
和setInterval
中的操作放入到timers队列中,将setImmediate
中的操作放入到check队列中,然后进入check阶段 - check
执行check队列中的setImmediate
中的操作 - close callbacks
执行时间关闭回调 - timers
执行timers队列中setTimeOut
和setInterval
中的操作 - I/O callbacks
执行系统操作回调事件
- 首先进入到poll阶段,查看poll队列中是否有任务,有任务的话就按照先进先出的顺序进行执行,当任务执行完毕,poll队列为空时候又或者是poll计时器到达最大限制时候(这个最大限时不是固定的),就将
-
setTimeOut执行超时问题
setTimeOut
的第二个参数用于设置执行延后的时间,以毫秒(ms)为单位,但是这个延时的操作其实并不准确,因为在poll阶段有可能因为被阻塞而导致延后时间边长,比如下面的例子就可以看出延时到了2800多毫秒才执行了setTimeOut
中的操作:
image.png
并且上面这个理由有个有趣的地方,当我不对数字n进行访问的时候,延时是2800多毫秒,当我在setTimeOut
中对n进行访问的时候,延时就变成了23000多毫秒,多了将近十倍:
image.png
这个也是说明了在不访问n的时候,poll阶段的操作其实并没有执行完,而是因为poll的计时器到达了最大值,所以poll阶段被强制终止进入了后续阶段,而我访问n的时候,这个计时器的功能就失效了,一定会算出n才会进行下一步,我猜测这也是为了保证操作的准确性而做出的优化吧。 -
setTimeOut和setImmediate执行顺序
关于这两者最重要的就是执行顺序问题了,其实从上面node的事件循环阶段就已经能够看出来了,setImmediate
是在setTimeOut
之前执行的,因为setImmediate
是在poll后面的check阶段执行,而setTimeOut
是在timers阶段执行,但是实际情况却有所不同,请看下面例子:
image.png
这是因为在主线程中,回调的执行顺序取决于当前进程的性能,所以顺序上会有不同,但是当我们将这两者都放入到同一个回调中去执行的时候,他们的顺序就能保证了,如下:
image.png
-
关于process.nextTick
另外在node中还有一个process.nextTick
用于推迟操作,这个process.nextTick
不存在于上面任何一个阶段,他只在当前阶段(无论是timers亦或是check等)结束后立即执行,可以从下面两个例子中看出:
image.png
image.png
后记
关于Event Loop的总结就到这里为止,这些在实际工作中用到的可能较少,面对面试时,主要还是在于区分好浏览器还是node环境,阐述清楚概念以及举出几个针对setTimeOut
、Promise
、async/await
、setImmediate
和process.nextTick
相关执行顺序即可。
网友评论