Node事件循环

发布于:2022-11-28 ⋅ 阅读:(378) ⋅ 点赞:(0)

node事件循环和浏览器事件循环完全不一样

Node事件循环阶段

  • timers(定时器):此阶段执行setTimeoutsetInterval调度的回调函数
  • I/O callback(I/O回调):此阶段执行几乎所有的回调函数,除了close callbacks(关闭回调) 和 由timers、setImmediate调度的回调
  • idle(空转):此阶段只在node内部使用
  • poll(轮询):检索新I/O事件,在恰当的时候Node会阻塞在这个阶段
  • check(检查):setImmediate的回调会在此阶段被调用
  • close callbacks(关闭事件的回调):例如socket.on('close', cb)的回调会在此阶段被调用

在这些阶段中,比较重要且复杂的是poll阶段

  • 如果poll队列不为空,将会同步的执行队列中的回调,直到队列为空或回调到达系统上限
  • 如果poll队列为空
    • 并且代码中有setImmediate的回调,那么将结束poll阶段,执行check阶段的队列(就是setImmediate的回调)
    • 并且代码中没有setImmediate的回调,事件循环会阻塞 在该阶段,等待回调加入到队列中,一旦有回调加入就执行
  • 如果进入poll阶段,且代码中有timers
    • 如果poll处于空闲,事件循环将会检查timers,如果有timers已经到了执行的时间,事件循环就会按顺序进入timers阶段,并执行timers队列(也就是进入下一阶段的事件循环

代码一

const fs = require('fs')
const path = require('path')

function asyncOperation(cb) { //模拟读取文件操作
  //假设读取文件花费2ms
  fs.readFile(path.resolve(__dirname, 'xx.txt'), cb)
}

setTimeout(() => {
  console.log('setTimeout');
}, 10)

asyncOperation(function() {
  let fileReadTime = Date.now()
  while(Date.now() - fileReadTime < 20) { //此处会卡住20ms

  }
})

分析:

  • 首先执行setTimeout,将回调放入I/O队列,他要10ms之后才执行,然后执行到asyncOperation函数,进行文件读取操作,需要2ms读取完成后执行回调,这个回调也放入到I/O中,所以此时timers阶段没有需要执行的回调。
  • 来到I/O callback(I/O回调),没有需要执行的回调
  • 来到poll阶段,现在的时间是0ms。此时poll队列为空,事件循环阻塞在poll阶段
  • 时间为2ms时,读取文件操作完毕,读取文件的回调进入poll队列被立即执行。而这个回调因为内部的while循环需要执行20ms
  • 到10ms时,setTimeout已经到了执行的时间,但是由于js为单线程且读取文件的回调占用了这个线程,所以setTimeout的回调无法执行
  • 当22ms时,读取文件的回调执行完毕,此时poll队列为空且有timers。所以setTimeout回调会被加入到第二轮事件循环的timers阶段中,然后执行这个回调

代码二

setTimeout(function timeout() {
  console.log('timeout');
}, 1)

setImmediate(function immediate() {
  console.log('immdeiate');
})

结果:上述代码的执行顺序不确定

不确定的原因:事件循环的启动也需要事件,可能执行到poll阶段已经超过1ms,此时setImmediate在check阶段被执行,先于setTimeout。反之setTimeout先执行

nextTick

process.nextTick()不在事件循环的任何阶段执行,而是在各个阶段切换的中间执行

设计原因:允许开发之通过调用process.nextTick()来阻塞I/O

示例代码:来看看下面代码的打印结果

const fs = require('fs')
const path = require('path')

fs.readFile(path.resolve(__dirname, 'read.txt'), () => { //假设读取文件耗时2ms
  setTimeout(() => {
    console.log('setTimeout');
  }, 0)

  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(() => {
      console.log('nextTick3');
    })
  })

  process.nextTick(() => {
    console.log('nextTick1');
  })

  process.nextTick(() => {
    console.log('nextTick2');
  })
})

//nextTick1
//nextTick2
//setImmediate
//nextTick3
//setTimeout

分析:

  • 由于读取文件需要耗时,所以在timers和I/O回调阶段没有要执行的回调,事件循环被阻塞在poll阶段
  • 当文件读取完成,将setTimeout的回调加入I/O队列。此时poll还是为空,且有setImmediate回调,所以从poll阶段要走到check阶段,在这期间执行两个nextTick,打印出nextTick1nextTick2
  • 来到check阶段,执行setImmediate回调,打印setImmediate
  • 从check阶段进入下一轮事件循环的timers阶段之间执行nextTick,打印nextTick3
  • 来到第二轮事件循环的timers,在I/O队列的setTimeout回调加入到timers中并执行,打印setTimeout

最后

我的博客网站上有我更多的学习成果

本文含有隐藏内容,请 开通VIP 后查看