node事件循环

2,252 阅读3分钟

事件循环是node处理非阻塞I/O操作的机制。

node.js启动后,会初始化事件轮询;执行脚本,然后启动事件循环。

概览

下图是事件循环处理顺序的概览:

每个阶段都有一个FIFO队列来执行回调。当事件循环进入一个阶段,会把该阶段的回调执行完或者达到最大回调数,才会移动到下一个阶段

阶段概述

  • timers: 执行setTimeout()setInterval()的回调函数。
  • pending callbacks: 执行延迟到下一次循环的I/O回调。
  • idle, prepare: 仅供内部使用
  • poll: 检索新的I/O事件;执行I/O相关回调。node将会在此处阻塞。
  • check: setImmediate()回调
  • close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

阶段详解

  • timers:给出可以执行回调的最少时间阈值。
  • poll: 有两个任务: 1)计算应该阻塞和轮询I/O 2)执行轮poll列里面的事件

当事件循环进入poll阶段,有两种可能: 1)poll队列不是空的,那么事件循环会将队列里面的回调一个一个执行,直到队列用尽,或者达到最高回调数。 2)poll是空的!,则会去检查有无check阶段回调等待,有的话,移动到check阶段执行。有无到达时间的timers回调,有的话,进入timer阶段执行。如果都没有,则会停留在这个poll阶段,等待回调,并且在有回调进入的时候,立即执行。

setImmediate()对比setTimeout()

处在事件循环不同阶段,执行时机不同。 使用 setImmediate()相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关,因为poll阶段之后,直接就是check阶段了。

process.nextTick()

process.nextTick()从技术上讲不是事件循环的一部分,因此并未出现在上图事件循环中。无论在哪个阶段,process.nextTick()回调都会在当前回调完成后处理。 这样机制下,如你过递归调用process.nextTick(),就会'饿死'正常I/O轮询里的回调。

代码示例

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

console.log('1')

let promise = new Promise(function(resolve){
  console.log('new promise')
  resolve(1)
})

fs.readFile(path.join(__dirname, './ling.js'), () => {
  console.log('readFile')
  process.nextTick(function(){
    console.log('tick insert')
  })
  promise.then(function(){
    console.log('promise insert')
  })
  setTimeout(() => {
    console.log('timeout')
    process.nextTick(function(){
      console.log('tick insert in timer')
    })
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

fs.readFile(path.join(__dirname, './ling.js'), () => {
  console.log('readFile2')
  setTimeout(() => {
    console.log('timeout2')
  }, 0)
  setImmediate(() => {
    console.log('immediate2')
  })
})


promise.then(function(){
  console.log('promise then1')
  promise.then(function(){
    console.log('promise then2')
    promise.then(function(){
      console.log('promise then3')
    })
  })
})

process.nextTick(function(){
  console.log('tick 1')
  process.nextTick(function(){
    console.log('tick 3')
  })
})
process.nextTick(function(){
  console.log('tick 2')
})

console.log('end')

执行结果

1
new promise // 同步执行,只有.then是异步
end // 第一次同步执行完毕
tick 1 // nextTick队列优先级最高,先一次性清空
tick 2
tick 3
promise then1 // 和nextTick类似,但是优先级没nextTick高
promise then2 // 也是先一次性清空
promise then3
readFile // 进入事件循环poll阶段
tick insert // 执行完poll阶段的一个回调之后,就会去检查nextTick
promise insert // 和 promise的队列有没有回调,有的话执行
readFile2 // 执行完上面的再继续poll阶段下一个回调
immediate // 进入check阶段
immediate2 // check阶段全部执行完
timeout // 进入timers阶段
tick insert in timer // tiemrs执行完一个回调,就会去检查nextTick,和promise
timeout2 // 继续执行timers阶段

和浏览器事件循环对比

差异: node的事件循环和浏览器的还是有差异的,特别是node事件循环存在几个阶段。

一致: 在处理promise.then 和process.nextTick 这些微任务,规则是一样的:就是在任何时候,只要处理完一个回调,就会去检查,有没有这些微任务,有的话,就一次性执行完毕。再回去继续原来。