什么是事件循环(Eventloop)
官网是这样描述的
事件循环是 Node.js 处理非阻塞 I/O 操作的机制——尽管 JavaScript 是单线程处理的——当有可能的时候,它们会把操作转移到系统内核中去。
既然目前大多数内核都是多线程的,它们可在后台处理多种操作。当其中的一个操作完成的时候,内核通知 Node.js 将适合的回调函数添加到 轮询 队列中等待时机执行。
事件循环有哪些阶段
- 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
- 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 检测:setImmediate() 回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)
一个6个阶段,每个阶段都会有类似于队列的概念
另外:
每个阶段都会检查是否有 process.nextTick 任务,如果有,全部执行
检查是否有microtask,如果有,全部执行
Timer
回调都被保存在最小堆中,如果符合条件就拿出来执行, 直到遇到一个不符合条件或者队列空了, 才结束 Timer
每次进入 Timer Phase 的时候都会保存一下当时的系统时间,然后只要看上述最小堆中的回调函数设置的启动时间是否超过进入 Timer Phase 时保存的时间, 如果超过就拿出来执行
Nodejs 为了防止某个 Phase 任务太多, 导致后续的 Phase 发生饥饿的现象, 所以消息循环的每一个迭代(iterate) 中, 每个 Phase 执行回调都有个最大数量. 如果超过数量的话也会强行结束当前 Phase 而进入下一个 Phase
Pending I/O(待定回调)
这一阶段是执行 fs.read, socket 等 IO 操作的回调函数, 同时也包括各种 error 的回调
Poll(轮询)
它首先会判断后面的 Check Phase 以及 Close Phase 是否还有等待处理的回调。如果有, 则不等待, 直接进入下一个 Phase。
如果没有,会在一段时间后进入下一个阶段。
Check(检测)
只处理 setImmediate 的回调函数
常见问题
- setTimeout(…, 0) vs. setImmediate
setTimeout(…, 0)vs. setImmediate 到底谁快?
// index.js
setImmediate(() => console.log(2))
setTimeout(() => console.log(1))
答案: 可能是 1 2, 也可能是 2 1
setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
setTimeout() 在最小阈值(ms 单位)过后运行脚本
当执行到 Timer Phase 时, 会发生两种可能. 因为每一轮迭代刚刚进入 Timer Phase 时会取系统时间保存起来, 以 ms(毫秒) 为最小单位.
如果 Timer Phase 中回调预设的时间 > 消息循环所保存的时间, 则执行 Timer Phase 中的该回调. 这种情况下先输出 1, 直到 Check Phase 执行后,输出2.总的来说, 结果是 1 2.
如果运行比较快, Timer Phase 中回调预设的时间可能刚好等于消息循环所保存的时间, 这种情况下, Timer Phase 中的回调得不到执行, 则继续下一个 Phase. 直到 Check Phase, 输出 2. 然后等下一轮迭代的 Timer Phase, 这时的时间一定是满足 Timer Phase 中回调预设的时间 > 消息循环所保存的时间 , 所以 console.log(1) 得到执行, 输出 1. 总的来说, 结果就是 2 1.