Node.js事件循环
事件循环同属来说就是一个无限的while循环
Node.js循环原理
可以看到,这一流程包含6个阶段,每个阶段代表的含义如下表示:
- timers: 本阶段已经被 setTimeout() 和 setInterval() 调度的回调函数,简单理解就是由这两个函数启动的回调函数
- pending callbacks: 本阶段执行某些系统操作 (如 TCP类型错误)的回调函数
- idle、prepare:仅系统内部使用,你只需要知道有这 2 个阶段就可以
- poll:检索新的 I/O 事件,执行与 I/O 相关的回调,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行,接下来会详细分析这个过程。
- check:setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分,如下代码所示:
1 | const fs = require('fs'); |
在这一行代码中又一个非常奇特的地方,就是 setImmediate会在 setTimeout之后输出。有以下几点原因
- setTimeout如果不设置时间活着设置时间为0,则会默认为1ms
- 主流程执行完成后,超过1ms时,会将setTimeout回调函数鹿皮插入到代执行poll队列中;
- 由于当前 pull队列 存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate。因此这也验证了这句话,先执行回调函数,再执行 setImmediate。
- close callbacks:执行一些关闭的回调函数,如 socket.on(‘close’, …)。
运行七点
从图一中可以看出事件循环的七点是timers,如下代码所示:
1 | setTimeout(() => { |
在代码 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个问题,事件循环中的事件类型是存在优先级