下面的分析都是基于 Node.js 10+ 版本的

1、Node.js循环原理图

node笔记之事件循环 - 图1

  • timers 阶段:这个阶段执行 setTimeout 和 setInterval 的回调函数。
  • pending callbacks 阶段:不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了所有回调函数。
  • idle, prepare 阶段:系统内部使用
  • poll 阶段:获取新的 I/O 事件。在某些场景下 Node.js 会阻塞在这个阶段。
  • check阶段:执行 setImmediate() 的回调函数。
  • close callbacks 阶段:执行关闭事件的回调函数,如 socket.on(‘close’, fn) 里的 fn。

执行代码过程中,大部分时间会停留在poll阶段,不断的对操作系统进行轮询。

当node程序结束时,Node.js会检查EventLoop是否在等待异步I/O结束,是否在等待计时器触发,如果没有,就会关掉 event loop。

2、运行起点

  1. setTimeout(() => {
  2. console.log('1')
  3. }, 0)
  4. console.log('2')
  5. // 2
  6. // 1

看事件循环的起点是timer阶段,但是我们这里先输入的是2?

原因是Node.js启动后,会初始化事件循环,处理已提供的脚本,它可能会先调用一些异步的 API、调度定时器,或者 process.nextTick(),然后再开始处理事件循环。那么运行的起点:Node.js进程启动后,就发起一个新的事件循环

3、Node.js 事件循环(poll阶段)

下面这个过程就是poll阶段了,主要处理异步I/O的回调函数,异步I/O在Node中一般分为网络I/O和文件I/O

node笔记之事件循环 - 图2

微任务:在 Node.js 中微任务包含 2 种——process.nextTick 和 Promise。微任务在事件循环中优先级是最高的,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise 也存在优先级,process.nextTick 高于 Promise

宏任务:在 Node.js 中宏任务包含 4 种——setTimeout、setInterval、setImmediate 和 I/O。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列

4、setImmediate vs setTimeout

两者区别在于回调时机不一致,setImmediate() 的作用是在当前 poll 阶段结束后调用一个函数,setTimeout() 的作用是在一段时间后调用一个函数。

如果 setTimeout 和 setImmediate 都是在主模块(main module)中被调用的,那么回调的执行顺序取决于当前进程的性能,两个回调的执行顺序我们无法判断

  1. setTimeout(() => {
  2. console.log('timeout');
  3. }, 0);
  4. setImmediate(() => {
  5. console.log('immediate');
  6. });

如果是刚进入程序,那么按照上面的流程图,此时限制性timeout,后执行immediate;若执行这段代码的时候event loop处于poll状态(大部分时间如此),那么就限制性immediate,后执行timeout

我们将上面的代码放到IO操作中进行回调,setImmediate的回调总是犹豫setTimeout的回调

  1. const fs = require('fs');
  2. fs.readFile(__filename, () => {
  3. setTimeout(() => {
  4. console.log('timeout');
  5. }, 0)
  6. setImmediate(() => {
  7. console.log('immediate');
  8. })
  9. })

5、单线程/多线程

主线程是单线程执行的,但是 Node.js 存在多线程执行,多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化等。

主要是主线程来循环遍历当前的事件,因此我们常说Node.js 是以事件驱动,来处理I/O密集型的高并发的语言