Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node零碎之事件循环机制 #52

Open
kekobin opened this issue Oct 11, 2019 · 0 comments
Open

Node零碎之事件循环机制 #52

kekobin opened this issue Oct 11, 2019 · 0 comments

Comments

@kekobin
Copy link
Owner

kekobin commented Oct 11, 2019

从一个例子开始

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

Javascript零碎之事件循环机制 可以很简单的得到结果:
image

然而,相同的代码,在node中运行后却得到了不一样的结果:
image

为什么同样都是单线程的事件循环机制有这么大的差异呢?

Node.js的事件循环机制

当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程

事件循环阶段

image

每一个阶段都有一个FIFO的callbacks队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.

  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

timers 阶段

timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 的执行顺序是不确定的。

setTimeout(() => {
  console.log('timeout')
}, 0)

setImmediate(() => {
  console.log('immediate')
})

技术上来说,poll 阶段控制 timers 什么时候执行。

但是如果把它们放到一个I/O回调里面(比如fs.readFile的回调),就一定是 setImmediate() 先执行,因为poll阶段后面就是check阶段。

I/O callbacks阶段

这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行.
名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.

poll 阶段

poll 阶段主要有2个功能:

  • 处理 poll 队列的事件
  • 当有已超时的 timer,执行它的回调函数

even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:

1.若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
2.若没有预设的setImmediate(),event loop将阻塞在该阶段等待

注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。

check 阶段

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

close callbacks 阶段

如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发

例子说明

const fs = require('fs')

fs.readFile('test.txt', () => {
  console.log('readFile')
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

由于是在IO的回调里,所以会立马去到poll阶段进行处理(I/O回调会由poll阶段处理).
得到如下执行结果:

readFile
immediate
timeout

node中的macro task 和 micro task

在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

image

开头例子的说明

回到开头的例子,现在应该能清楚的知道为什么输出结构会和浏览器上的有那么大的差别了:

image

全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行;
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。

process.nextTick() VS setImmediate()

process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.它一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段。

而setImmediate的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.

const fs = require('fs')
const starttime = Date.now()
let endtime

fs.readFile('text.txt', () => {
  endtime = Date.now()
  console.log('finish reading time: ', endtime - starttime)
})

let index = 0

function handler () {
  if (index++ >= 1000) return
  console.log(`nextTick ${index}`)
  process.nextTick(handler)
  // console.log(`setImmediate ${index}`)
  // setImmediate(handler)
}

handler()

结果是:

nextTick 1
nextTick 2
......
nextTick 999
nextTick 1000
finish reading time: 170

替换成setImmediate(),运行结果:

setImmediate 1
setImmediate 2
finish reading time: 80
......
setImmediate 999
setImmediate 1000

遇到IO回调会去到poll阶段,然后会查看是否有setImmediate,发现有,会先执行setImmediate,在某一间隙,setImmediate执行完了后,poll会执行IO回调,然后继续往下。
所以递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()。

参考

深入理解js事件循环机制(Node.js篇
nodejs理解事件循环机制

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant