事件循环是 Node.js 处理非阻塞 I/O 操作的机制,node.js是单线程的,同一时间只会执行一份js代码。但这并不意味着node.js很慢,遇到耗时任务,我们不可能阻塞单线程的运行,而是把此任务扔给系统内核去处理,主线程继续处理请求服务,这样可以高并发运行业务。而在系统内核中,会有多线程操作来处理node.js任务,当处理完毕后,扔回事件循环队列里。等到主线程处理完所有的同步的js代码后,会从事件循环队列里取出任务,执行对应的回调函数。
事件轮询机制解析
node.js是基于V8引擎的javascript运行环境,基于事件驱动,底层用libuv库进行异步事件处理。
从上图可以看到事件循环的过程,接下来根据官网文档,具体分析一下。
在node.js启动后,会初始化事件循环
下面的图表显示了事件循环的概述以及操作顺序,从图中看到事件循环有6个阶段,每个阶段对应一个先进先出的回调队列。
1 | ┌───────────────────────────┐ |
- timers(定时器): 本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数。
- pending callbacks(待定回调): 执行延迟到下一个循环迭代的 I/O 回调,除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
- idle, prepare: 仅系统内部使用
- pool(轮询):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
- check(检测):setImmediate() 回调函数在这里执行
- close callbacks(关闭的回调函数): 一些准备关闭的回调函数,如:socket.on(‘close’, …)
timers(定时器)
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
Note: 轮询阶段控制何时定时器执行。
pending callbacks(待定回调)
此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED,则某些 *nix 的系统希望等待报告错误。这将被排队以在此阶段执行。
pool(轮询)
轮询阶段有2个重要的功能:
- 执行下限时间已经达到的timers的回调
- 然后,处理轮询队列里的事件
当事件循环进入poll阶段,且没有发现timers(定时器)时:
- 如果轮询队列不空,事件循环会遍历队列并同步执行回调,直到队列清空或执行的回调数达到系统上限;
- 如果轮询队列为空,则发生以下两件事之一:
- 如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调
- 如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行
如果事件循环进入poll阶段,且有设定的timers,一旦poll队列为空:
- 事件循环将检查timers,如果有1个或多个timers的下限时间已经到达,事件循环将回到timers(定时器)阶段执行回调。
Q:
- poll队列不空,达到执行上限后去哪里?执行主线程同步代码,执行完毕后,继续执行poll阶段队列。
- poll阶段,有timers,但下限时间未到达,回到那个阶段?检测阶段,等待队列,处理队列…时间到,满足条件后回到timers阶段。
check(检测)
此阶段在poll阶段结束后立即执行回调(poll空闲,且有被setImmediate()设定的回调),事件循环会转到check(检测)阶段而不是继续等待。
根据官网所说,setImmediate是一个特殊的timer,跑在事件循环中一个独立的阶段。使用libuv的API来安排回调在轮询阶段完成后执行。
close callbacks(关闭的回调函数)
如果套接字或处理函数突然关闭(例如 socket.destroy()),则’close’ 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。
setTimeout 与 setImmediate
- setImmediate()被设计在 poll 阶段结束后立即执行回调;
- setTimeout()被设计在指定下限时间到达后执行回调;
执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时将受进程性能的约束(这可能会受到计算机上运行的其它应用程序的影响)。下面脚本的执行顺序就是非确定性的,受进程性能的约束:
无I/O处理情况下:1
2
3
4
5
6
7setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:1
2
3
4
5
6
7
8
9
10
11// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
setImmediate() 比 setTimeout() 优势的地方是 setImmediate() 在 I/O 循环中总是先于任何定时器,不管已经定义了多少定时器。
process.nextTick()
在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前”
关于 setTimeout(), setImmediate(), process.nextTick():
- setTimeout() 在某个时间值过后尽快执行回调函数;
- setImmediate() 一旦轮询阶段完成就执行回调函数;
- process.nextTick() 在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前” ;
优先级顺序从高到低: process.nextTick() > setImmediate() > setTimeout()
注:这里只是多数情况下,即轮询阶段(I/O 回调中)。比如之前比较 setImmediate() 和 setTimeout() 的时候就区分了所处阶段/上下文。
参考:
https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/
https://segmentfault.com/a/1190000012258592