【Javascript】Event loop 在浏览器端与NodeJS中的差别 以及 NodeJS中关于setTimeout与setImmediate引发的问题
AwesomeDevin opened this issue · comments
组内每周都会有分享总结会,昨晚分享的课题是Event Loop
,我很积极且有点自信地答了几道题,结果被虐的体无完肤
,果然有些东西不经常回顾就容易忘,于是花一晚上挑灯夜战
重新做了一份有关event loop
的知识总结,在此分享给大家,希望对各位看官有所帮助,看完有收获的同学还请积极点赞,讲的不对的地方,望指出,我会及时修正,谢谢~
浏览器端
浏览器端的event loop基于javascript中的
堆/栈/任务队列
,任务队列又分为宏任务
与微任务
每次事件循环的时候:
- 微任务/宏任务在
相同作用域下
,会先执行微任务,再执行宏任务 宏任务处于微任务作用域下
,会先执行微任务,再执行微任务中的宏任务微任务处于宏任务作用域下时
,会先执行宏任务队列中的任务,然后再执行微任务队列中的任 务,在当前的微任务队列没有执行完成时,是不会执行下一个宏任务的。
本文主要讲解的还是Node,对于浏览器端event loop的具体分析及证明可以查看这篇文章探究javascript中的堆/栈/任务队列与并发模型 event loop的关系
Nodejs端
nodejs 的事件循环分为6个阶段
,每个阶段都有1个任务队列,微任务在事件循环的各个阶段之间执行
timers 阶段: 这个阶段执行timer(setTimeout、setInterval)的回调
一个timer
指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定的时间过后,timers
会尽早的执行回调,但是系统调度或者其他回调的执行可能会延迟它们。
从技术上来说,
poll
阶段控制timers
什么时候执行,而执行的具体位置在timers
。
下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。
I/O callbacks 阶段:执行大多回调以及一些系统调用错误,比如网络通信的错误回调
idle, prepare 阶段: 仅node内部使用
poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
poll阶段有两个主要的功能:
1. 是执行下限时间已经达到的timers的回调
2. 是处理poll队列里的事件。
注:Node很多API都是基于
事件订阅
完成的,这些API的回调应该都在poll
阶段完成。
当事件循环进入poll阶段:
-
poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
-
poll队列为空的时候,这里有两种情况。
-
如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
-
如果代码没有被设定setImmediate()设定回调:
- 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
- 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。
-
check 阶段:执行 setImmediate() 的回调
这个阶段允许在poll阶段结束后立即执行回调。如果poll阶段空闲,并且有被setImmediate()设定的回调,那么事件循环直接跳到check执行而不是阻塞在poll阶段等待回调被加入。
注:事件循环运行到
check
阶段的时候,setImmediate()
具有最高优先级,只要poll
队列为空,代码被setImmediate()
,无论是否有timers
达到下限时间,setImmediate()
的代码都先执行
close callbacks 阶段:执行 socket 的 close 事件回调
如果一个socket
或handle
被突然关掉(比如socket.destroy()
),close事件将在这个阶段被触发,否则将通过process.nextTick()
触发。
NodeJS中关于setTimeout与setImmediate引发的问题
问题引入
setTimeout(()=>{
console.log('timer')
})
setImmediate(()=>{
console.log('immediate')
})
原因
首先进入的是timers
阶段,如果我们的机器性能一般,那么进入timers阶段,一毫秒已经过去了,那么setTimeout的回调会首先执行。
如果没有到一毫秒,那么在timers阶段的时候,下限时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),所以进入check阶段,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。
而我们在执行代码的时候,进入timers的时间延迟其实是随机的,并不是确定的,所以会出现两个函数执行顺序随机的情况。
我们再来看一段代码
fs.readFile('./main.js',()=>{
setTimeout(()=>{
console.log('timer')
})
setImmediate(()=>{
console.log('immediate')
})
})
可以发现setImmediate
永远先于setTimeout
执行
原因
fs.readFile
的回调是在poll
阶段执行的,回调执行完毕后poll阶段的队列为空,于是进入check
阶段,执行setImmediate
回调,而setTimeout
的回调需要等到下一个事件循环的timers
阶段才去执行
NodeJS中的process.nextTick() and Promise
对于这两个,我们可以把它们理解成一个微任务。也就是说,它其实不属于事件循环的一部分。
那么他们是在什么时候执行呢?
不管在什么地方调用,他们都会在其所处的事件循环最后,在事件循环进入下一个循环的阶段前执行,但是
nextTick
优先于promise
执行。
process.nextTick()
会在各个事件阶段之间执行,一旦执行,要直到nextTick
队列被清空,才会进入到下一个事件阶段,所以如果递归调用process.nextTick()
/promise
,会导致出现I/O starving(饥饿)的问题,推荐使用setImmediate()
看了这么多,现在大家做两道题吧,检测自己是否真的理解了
question1
setTimeout(() => {
console.log('timeout1')
Promise.resolve().then(()=>{
console.log('reslove1')
})
})
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(()=>{
console.log('reslove2')
})
})
setImmediate(()=>{
console.log('setImmediate1')
})
setImmediate(()=>{
console.log('setImmediate2')
})
question2
setTimeout(() => {
console.log('timeout1')
Promise.resolve().then(()=>{
console.log('reslove1')
})
})
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(()=>{
console.log('reslove2')
})
})
setImmediate(()=>{
console.log('setImmediate1')
})
setImmediate(()=>{
console.log('setImmediate2')
})
Promise.resolve('resolve3').then((data)=>{
console.log(data)
})
答案
question1
有两种可能性
- 第一种
timeout1 > timeout2 > resolve1 > resolve2 > setImmediate1 > setImmediate2
- 第二种
setImmediate1 > setImmediate2 > timeout1 > timeout2 > resolve1 > resolve2
question2
只有一种可能性
resolve3 > timeout1 > timeout2 > resolve1 > resolve2 > setImmediate1 > setImmediate2