理解 Node中的事件循环
很久没有写过关于node的文章了,于是即兴写一笔,是对官方文档的一个理解,也是对自己的学习做一个巩固。希望能够帮到大家
事件循环
node启动时会初始化一个事件循环,事件循环不会单独开一个线程,而是挂载到主线程。
node的事件循环与前端js的事件循环有些不一样,node事件循环分为6个阶段执行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
idle, prepare 阶段: 仅node内部使用;
poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
check 阶段: 执行setImmediate() 设定的callbacks;
close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.
每一个阶段有一个队列,event loop执行到该阶段时,会该阶段的队列里的所有callback,当队列callback为空时,或callback执行到上限的时,就跳至下一阶段进行执行
其中poll阶段除了执行当前阶段的队列里的所有callback之外,poll还负责检测是否有timer的callback,如timer到达时间,并且timer的callback还未执行,那么就循环至开头执行timer的callback
定时器的执行
先来看一个例子
const fs = require('fs');
const path = require('path')
function asyncSomething(cb){
fs.readFile(path.join(__dirname, __filename),cb)
}
let timeoutSchedule = Date.now()
setTimeout(() => {
let delay = Date.now() - timeoutSchedule;
console.log(`timeout spend time ${delay} ms`)
},10)
asyncSomething(() => {
let readFileTime = Date.now()
console.log(`readFile spend time ${readFileTime - timeoutSchedule} ms`);
while(Date.now() - timeoutSchedule < 20){}
})
输出
readFile spend time 1 ms
timeout spend time 21 ms
初始化后的event loop,首先进入timer,但是setTimeout指定的定时器10ms过后执行,因此继续进入下一阶段,一直到poll阶段,readFile执行1ms后完毕,readFile的callback延时20ms后,此时timer指定的定时器已经超时,马上循环到timer,执行定时器的callback,打印出来21ms。
然而如果定时器指定0ms后执行,那么event loop最开始进入timer就会执行timer指定的定时器的callback
比如将上述代码的setTimeout的延时时间改为0
const fs = require('fs');
const path = require('path')
function asyncSomething(cb){
fs.readFile(path.join(__dirname, __filename),cb)
}
let timeoutSchedule = Date.now()
setTimeout(() => {
let delay = Date.now() - timeoutSchedule;
console.log(`timeout spend time ${delay} ms`)
},0)
asyncSomething(() => {
let readFileTime = Date.now()
console.log(`readFile spend time ${readFileTime - timeoutSchedule} ms`);
while(Date.now() - timeoutSchedule < 20){}
})
输出
timeout spend time 1 ms
readFile spend time 3 ms
在event loop首次进入timer的时候就执行了timeout指定的callback,然后event loop往下倒poll阶段,执行readFile,但是由于timeout处于主模块下,系统的其他进程会影响到它,因此定时器不一定是进入初始化的event loop的timer,因此有时候,打印出来的结果会是:
readFile spend time 1 ms
timeout spend time 20 ms
Timeout和Immediate
在主模块下,同样受系统其他进程的影响,可能timeout不能进入第一次event loop的timer阶段,所以可能是处于check阶段的immediate先执行,也可能是处于timer阶段的timeout先执行。
setTimeout(() => console.log("timeout"),0)
setImmediate(() => console.log("immediate"))
输出的两种情况
timeout
immediate
或者
immediate
timeout
但是如果是处于其他的callback内,会是怎样的情况呢?
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname,__filename),() => {
setTimeout(() => console.log("timeout"),0)
setImmediate(() => console.log("immediate"))
})
这样不管怎样,immediate总比timeout先执行
因为当readFile的回调触发时,此时处于event loop的poll阶段,由于check阶段设置有immediate,进入check阶段执行immediate,然后再回到timer阶段,执行timeout,由此输出
immediate
timeout
process.nextTick
这个函数可以很快速的把回调函数加入事件循环
值得注意的是,process.nextTick不属于event loop的任何阶段,它们会在两个阶段过渡的时候执行,即在第二个阶段执行之前执行。
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname,__filename),() => {
process.nextTick(() => console.log("nextTick1"))
setTimeout(() => console.log("timeout"),0)
setImmediate(() => {
console.log("immediate")
process.nextTick(() => console.log("nextTick4"))
process.nextTick(() => console.log("nextTick5"))
})
process.nextTick(() => console.log("nextTick2"))
process.nextTick(() => console.log("nextTick3"))
})
输出
nextTick1
nextTick2
nextTick3
immediate
nextTick4
nextTick5
timeout
首先readFile的callback触发的时候,处于eventLoop的poll阶段,poll到check的过渡阶段执行nextTick1,nextTick2,nextTick3,之后进入check阶段,触发immediate,发现timer有定时器需要触发,所以该由check阶段进入timer阶段,在过渡的过程中,nextTick4,nextTick5触发,最后打印出timeout
process.nextTick是immediate还未出现的产物,因此node的作者建议我们使用immediate,比如大量的process.nextTick,将使我们无法进入event loop的下一阶段