0%

事件循环:高性能到底是如何做到的

Node.js事件循环

事件循环同属来说就是一个无限的while循环

Node.js循环原理

可以看到,这一流程包含6个阶段,每个阶段代表的含义如下表示:

  1. timers: 本阶段已经被 setTimeout() 和 setInterval() 调度的回调函数,简单理解就是由这两个函数启动的回调函数
  2. pending callbacks: 本阶段执行某些系统操作 (如 TCP类型错误)的回调函数
  3. idle、prepare:仅系统内部使用,你只需要知道有这 2 个阶段就可以
  4. poll:检索新的 I/O 事件,执行与 I/O 相关的回调,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行,接下来会详细分析这个过程。
  5. check:setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分,如下代码所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
console.log('1');
}, 0);
setImmediate( () => {
console.log('setImmediate 1');
});
/// 将会在 poll 阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在这一行代码中又一个非常奇特的地方,就是 setImmediate会在 setTimeout之后输出。有以下几点原因

  • setTimeout如果不设置时间活着设置时间为0,则会默认为1ms
  • 主流程执行完成后,超过1ms时,会将setTimeout回调函数鹿皮插入到代执行poll队列中;
  • 由于当前 pull队列 存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate。因此这也验证了这句话,先执行回调函数,再执行 setImmediate。
  • close callbacks:执行一些关闭的回调函数,如 socket.on(‘close’, …)。

运行七点

从图一中可以看出事件循环的七点是timers,如下代码所示:

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

在代码 setTimeout 中的回调函数就是新一轮事件循环的起点,看到这里有很多同学会提出非常合理的疑问:“为什么会先输出 2 然后输出 1,不是说 timer 的回调函数是运行起点吗?”

这里有一个非常关键点,当 Node.js 启动后,会初始化事件循环,处理已提供的输入脚本,它可能会先调用一些异步的 API、调度定时器,或者 process.nextTick(),然后再开始处理事件循环。因此可以这样理解,Node.js 进程启动后,就发起了一个新的事件循环,也就是事件循环的起点。

总结来说,Node.js 事件循环的发起点有 4 个:

  • Node.js启动后
  • setTimeout回调函数
  • setInterval回调函数
  • 也可能是一次I/O后的回调函数

Node.js事件循环

在上面的核心流程中真正需要关注循环执行的就是 poll 这个过程。在poll过程中,主要处理的是异步 I/O 的回调函数,以及其他几乎所有的回调函数,异步I/O又分为网络I/O和文件I/O。这是我们常见的代码路基部分的异步回调逻辑

微任务和红任务

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

宏任务: 在node.js中宏任务包含4种 - setTimeout、setInterval、setImmediate 和 I/O。宏任务在微任务执行之后执行,因此在统一事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列。这也解析了我们前面提到的第3个问题,事件循环中的事件类型是存在优先级