Node.js-Event-Loop

Node 事件循环(Event-Loop)

Node.js 是通过事件驱动来服务 I/O 的,而事件循环是 Node 实现异步非阻塞 I/O 的基础

Node 事件循环是基于 libuv 实现的,而内部实现,其实就是一个 while 语句块
Node 程序启动时,则会初始化事件循环

这里需要说明下,Node 事件循环与浏览器环境下 JavaScript 的事件循环是完全不一样的

浏览器环境下 JS 事件循环

Node 事件循环是分阶段的,有 update time、timer、 pending (I/O) callbacks、idle, prepare、poll(轮询)、check、close callbacks 七个阶段

而这七个阶段都是在核心函数 uv_run() 中进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// Node 事件循环核心函数
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r) // 检查当前循环是否存活
uv__update_time(loop); // 如果没有则更新时间并结束当前循环,进入下一个循环
// Node 事件循环从一个 while 循环开始
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop); // 更新时间
uv__run_timers(loop); // 执行到期的定时器(setTimeout 和 setInterval)回调(往往是之前n轮循环中设置的定时器)
ran_pending = uv__run_pending(loop); // 处理异步 I/O,如网络请求 I/O、文件请求 I/O 等
uv__run_idle(loop);
uv__run_prepare(loop);
// idle 和 prepare 是 Node 内部的一些操作,跟事件循环没有什么关联
timeout = 0; // 设置 poll 阶段的时间变量
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop); // 计算 timeout 时间,用于后面 poll 阶段的操作
uv__io_poll(loop, timeout); // 执行 poll 阶段的相关回调,此阶段有可能会阻塞
uv__run_check(loop); // 执行 setImmediate 回调
uv__run_closing_handles(loop); // 执行回调函数关闭的方法,如关闭文件描述符
// Node默认的mode是 `UV_RUN_ONCE`
if (mode == UV_RUN_ONCE) {
uv__update_time(loop); // 更新时间
uv__run_timers(loop); // 执行到期的定时器回调
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
} // while 循环结束
if (loop->stop_flag != 0)
loop->stop_flag = 0;
return r;
}
// 计算 poll 阶段的时间参数
int uv_backend_timeout(const uv_loop_t* loop) {
if (loop->stop_flag != 0) // 事件循环是否结束的标志,如果结束则返回0
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) // 是否有异步任务,没有则返回0
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
// QUEUE_EMPTY(idle_handles)和QUEUE_EMPTY(pending_queue)
// 判断前面阶段注册的异步任务队列是否为空,则不为空直接返回0,执行这些异步任务回调
if (loop->closing_handles) // 循环进入关闭阶段,返回0
return 0;
// 如果上述条件都不成立,那么 poll 阶段将等待第一个到期的 timer,如果在极短的时间(32.767毫秒)内没有到期的 timer ,则结束等待,进入 下一个阶段(check阶段)
return uv__next_timeout(loop);
}

各阶段简述

  • update time:更新时间
  • timer:执行到期的定时器回调函数
  • pending (I/O) callbacks:执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare:Node 内部执行的
  • poll:检索新的 I/O事件;执行I / O相关的回调函数(几乎所有函数的回调函数,除了定时器和setImmediate);此外,适当时,节点将在此处阻塞
  • check:执行 setImmediate 回调
  • close callbacks:一些关闭回调,如 socket.on(‘close’, …)

poll 阶段(重点理解)

poll阶段有两个功能:

  • 当 timers 到达指定的时间后,执行指定的 timer 的回调
  • 处理 poll 队列的事件

1、当进入到 poll 阶段,并且没有 timers 被调用的时候,会发生下面的情况:

  • 如果 poll 队列不为空,Event Loop 将同步的执行 poll queue 里的 callback ,直到 queue 为空或者执行的 callback 到达上限
  • 如果 poll 队列为空,则会发生下面的情况:
  • 如果脚本调用了 setImmediate(),Event Loop 将会结束 poll 阶段并且进入到 check 阶段执行 setImmediate() 的回调
  • 如果脚本没有被 setImmediate() 调用,Event Loop 将会等待(此时会阻塞极短时间32.767毫秒)回调被添加到队列中,然后立即执行它们

2、当进入到 poll 阶段,并且调用了 timers 的话,会发生下面的情况:

  • 一旦 poll queue 是空的话,Event Loop 会检查是否 timers,如果有1个或多个 timers 时间已经到达,Event Loop 将会回到 timer 阶段并执行那些 timers 的 callback (即进入到下一次 tick)。

Pending callbacks(即I/O callbacks)被调用,大多数情况下,所有的 I/O callbacks 都是在 poll for I/O(即 poll 阶段)后理解调用的。然而,有些情况,会在下一次 tick 调用,以前被推迟的 I/O callback 会在下一次 tick 的 I/O 阶段调用。

在 Event Loop 完成一个阶段,然后到另一个阶段之前,Event Loop 将会执行这 Next Tick Queue 以及 MicroTask Queue 里面的回调,直到这两个队列为空。一旦它们空了后,Event Loop 会进入到下一个阶段

Next Tick Queue的优先级高于MicroTask Queue

setImmediate() vs setTimeout()

setImmediate和setTimeout()是相似的,但取决于它们何时被调用,其行为方式不同。

  • setImmediate()用于在当前 poll(轮询)阶段完成后执行脚本
  • setTimeout()设置在一定时间(以毫秒为单位)后运行

定时器执行的顺序取决于它们被调用的上下文
如果在 I/O 周期内这两个被调用,则 setImmediate 回调总是先执行

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

如果它们不在 I/O 周期内被调用,则两者执行顺序是不确定的

process.nextTick() vs setImmediate()

process.nextTick() 属于 idle 观察者,setImmediate() 属于 check 观察者,idle 观察者优先级大于 I/O 观察者,I/O 观察者优先级大于 check 观察者,因此 process.nextTick() 总是先执行

参考 The Node.js Event Loop, Timers, and process.nextTick()

坚持原创技术分享,您的支持将鼓励我继续创作!