本文同步发布于 个人博客javascript
首先全部平台,不管是浏览器仍是 nodejs
的 JS
事件循环都不是由 ECMA 262
规范定义。事件循环并非 ECMA 262
规范的一部分。浏览器端的事件循环由 Web API
中定义,并由 W3C
和 HTML living standard
来维护。而 nodejs
是基于 libuv
的事件循环,其并无一个事件循环规范标准,那么了解 nodejs
事件循环的最好方式就是 nodejs
的源码和官方文档和 libuv
的源码和官方文档。html
文章中引用的参考尽量选取官方文档、
nodejs/libuv
仓库,nodejs/libuv
贡献者解答,google/microsoft
工程师,高赞stackoverflow
回答等来源。java
根据 nodejs
官方文档,在一般状况下,nodejs
中的事件循环根据不一样的操做系统可能存在特殊的阶段,但整体是能够分为如下 6
个阶段:node
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
复制代码
timer
阶段,用于执行全部经过计时器函数(即 setTimeout
和 setInterval
)注册的回调函数。linux
pending callbacks
阶段。虽然大部分 I/O
回调都是在 poll
阶段被当即执行,可是会存在一些被延迟调用的 I/O
回调函数。那么此阶段就是为了调用以前事件循环延迟执行的 I/O
回调函数。git
引自在
libuv
的设计文档 the I/O loop - step.4。github
idle prepare
阶段,仅用于 nodejs
内部模块使用。web
poll
(轮询)阶段,此阶段有两个主要职责:1. 计算当前轮询须要阻塞后续阶段的时间;2. 处理事件回调函数。c#
nodejs
中事件循环中存在一种维持在此阶段的趋势,后文会作详细说明。浏览器
check
阶段,用于在 poll
阶段的回调函数队列为空时,使用 setImmediate
实现调度执行特定代码片断。
close
回调函数阶段,执行全部注册 close
事件的回调函数。
每个 nodejs
事件循环 tick
老是要经历以上阶段,由 timer
阶段开始,由 close
回调函数阶段结束。每个阶段都会循环执行当前阶段的回调函数队列,直至队列为空或到达最大可执行回调函数次数。
据 nodejs
官方文档,nodejs
中的事件循环是依赖于名为 libuv 的 C
语言库实现。本质上 libuv
的执行方式决定了 nodejs
中的事件循环的执行方式。
至本文发布之际,最新
libuv
的版本为 v1.35.0.
Q: libuv
是什么?
A: libuv
是使用 C
语言实现的单线程非阻塞异步 I/O
解决方案,本质上它是对常见操做系统底层异步 I/O
操做的封装,并对外暴露功能一致的 API
, 首要目的是尽量的为 nodejs
在不一样系统平台上提供统一的事件循环模型。
nodejs
的事件循环核心对应 libuv
中的 uv_run 函数,核心逻辑以下:
// http://docs.libuv.org/en/v1.x/loop.html#c.uv_loop_alive
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while (r != 0 && loop->stop_flag == 0) {
// http://docs.libuv.org/en/v1.x/loop.html#c.uv_update_time
uv__update_time(loop);
// timer 阶段
uv__run_timers(loop);
// pending callbacks 阶段
ran_pending = uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
// poll 阶段
uv__io_poll(loop, timeout);
// check 阶段
uv__run_check(loop);
// close callbacks 阶段
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
/* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */
uv__update_time(loop);
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
复制代码
据 libuv
文档中对 IO loop 的描述,原则上,一个线程中至多仅有一个事件循环,在多个线程中能够存在多个并行的事件循环。事件循环遵循常规的单线程异步 I/O
方案。全部的 (网络)I/O
都是在非阻塞的 socket
上执行。这些 socket
使用了以下表给定平台的最佳轮询机制。
mechanism | platform |
---|---|
epoll | Linux |
kqueue | OSX, BSD |
IOCP | Windows |
event ports | SunOS |
单个事件循环 loop
做为整个事件循环迭代 loop iteration
的一部分,它会 阻塞 等待已经添加到 poller
中的 sockets
上的 IO
活动,并间触发对应的回调函数以指示 socket
的条件(便可读,可写,挂起),以便句柄能够读取,写入,或执行所指望的 IO
操做。
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
复制代码
根据源代码和 libuv
官方文档,事件循环首先会缓存当前事件循环 tick
的开始时间,用于减小时间相关的系统调用。
缓存时间的作法是由于系统内的时间调用会受到系统内其余应用的影响,因此为了尽量避免其余应用对
nodejs
的影响而在事件循环的tick
开始之时缓存时间。
若是事件循环是活动的,那么开始当前事件循环,不然当即退出整个事件循环迭代。那么如何界定一个事件循环迭代是活动的?若是一个事件循环拥有活动的句柄或引用句柄,活动的请求或 closing
句柄,那么该事件循环被认为是活动的。
// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while (r != 0 && loop->stop_flag == 0) {
// ...
}
复制代码
从以上示例代码不难看出,整个事件循环迭代就是一个 while
无限循环,正是这个 while
语句在不断地推进事件循环的迭代。在每一次循环迭代开始时,都会不断验证当前事件循环 tick
是不是活动的,且没有 stop
标识。在进入循环以后首先会更新当前事件循环的开始时间并继续执行事件循环的各个阶段的回调函数队列。
结合前文对 nodejs
中事件循环的生命周期抽象概括,不难依据 uv_run()doc 的核心逻辑得出:
timer
阶段: uv__run_timers(loop)
pending callbacks
阶段:uv__run_pending(loop)
idle
阶段:uv__run_idle(loop)
poll
阶段:uv__io_poll(loop, timeout)
check
阶段:uv__run_check(loop)
close callbacks
阶段:uv__run_closing_handles(loop)
函数定义
nodejs
事件循环的一个 tick
始终以 timer
阶段开始,其中包含一个由全部 setTimeout
和 setInterval
注册的待执行回调函数队列。此阶段的 核心职责 是执行由全部到达时间阈值的计时器注册的回调函数。
待执行,表示在已经到达计时器的时间阈值时,被加入到
timer
阶段的回调函数队列中等待执行的由计时器注册的回调函数。
值得声明的一点是,不管是在 nodejs
仍是 web
浏览器中,全部的计时器实现都 不能保证 在到达时间阈值后回调函数必定会被当即执行,它们只能保证在到达时间阈值后,尽快 执行由计时器注册的回调函数。
const NS_PER_SEC = 1e9
const time = process.hrtime()
// [ 1800216, 25 ]
setTimeout(() => {
const diff = process.hrtime(time)
// [ 1, 552 ]
console.log(`Benchmark took ${diff[0] * NS_PER_SEC + diff[1]} nanoseconds`)
// Benchmark took 1000000552 nanoseconds
}, 1000)
复制代码
另外,从技术上讲,poll
阶段决定了 timer
回调函数的执行时机。详情可见后文关于 poll 对 timer 的影响 的说明。
如前文所述,timer
阶段对应 libuv
中 C
函数为 uv__run_timers(loop);。且在 uv_run
函数体中对应的核心调用逻辑以下:
int timeout;
int r;
int ran_pending;
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop);
uv__run_timers(loop);
// ...
}
复制代码
在开始事件循环的一个 tick
时,老是会首先调用 uv__update_time(loop);
来更新当前事件循环 tick
的开始时间。
UV_UNUSED(static void uv__update_time(uv_loop_t* loop)) {
/* Use a fast time source if available. We only need millisecond precision. */
loop->time = uv__hrtime(UV_CLOCK_FAST) / 1000000;
}
复制代码
此处 uv__hrtime
函数内部包含当前操做系统的暴露的时间相关系统调用。在此处对系统的时间调用时,可能会受到其余其余应用的影响。一旦更新 loop
结构体的 time
后,接着会开始执行 timer
阶段的回调函数队列。以下:
void uv__run_timers(uv_loop_t* loop) {
struct heap_node* heap_node;
uv_timer_t* handle;
for (;;) {
heap_node = heap_min(timer_heap(loop));
if (heap_node == NULL)
break;
// container_of 由 preprocesser 来实现编译前文本替换
// https://github.com/libuv/libuv/blob/v1.35.0/src/uv-common.h#L57-L58
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout > loop->time)
break;
// http://docs.libuv.org/en/v1.x/timer.html#c.uv_timer_stop
uv_timer_stop(handle);
// http://docs.libuv.org/en/v1.x/timer.html#c.uv_timer_again
uv_timer_again(handle);
handle->timer_cb(handle);
}
}
复制代码
这里值得注意的时,全部计时器在 libuv
中是以计时器回调函数的 执行时间节点(即 time + timeout
,而不是计时器时间阈值) 构成的 二叉最小堆 结构来存储。经过 二叉最小堆
的根节点来获取时间线上最近的 timer
对应的回调函数的句柄,再经过该句柄对应的 timeout
值获取最近的计时器的执行时间节点:
当该值大于当前事件循环 tick
的开始时间时,即表示尚未到执行时机,回调函数还不该该被执行。那么根据二叉最小堆的性质,父节点始终比子节点小,那么根节点的时间节点都不知足执行时机的话,其余的 timer
时间节点确定也没有过时。此时,退出 timer
阶段的回调函数执行,进入事件循环 tick
的下一阶段。
当该值小于当前事件循环 tick
的开始时间时,表示至少存在一个过时的计时器,那么循环迭代计时器最小堆的根节点,并调用该计时器所对应的回调函数。每次循环迭代时都会更新最小堆的根节点为最近时间节点的计时器。
在现行 nodejs
中,有且仅有两种计时器,其中之一就是是 setTimeout/setInterval
。 在使用 setTimeout/setInterval
时,值得注意的一点是:
时间阈值的取值范围是 1 ~ 231-1 ms,且为整数。
在 nodejs/node 源码中不管是 setTimeout
源码实现 仍是 setInterval
源码实现本质上都是内置类 Timeout 的实例,以下:
// Timeout values > TIMEOUT_MAX are set to 1.
const TIMEOUT_MAX = 2 ** 31 - 1
// Timer constructor function.
// The entire prototype is defined in lib/timers.js
function Timeout(callback, after, args, isRepeat, isRefed) {
after *= 1 // Coalesce to number or NaN
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
if (after > TIMEOUT_MAX) {
process.emitWarning(
`${after} does not fit into` +
' a 32-bit signed integer.' +
'\nTimeout duration was set to 1.',
'TimeoutOverflowWarning'
)
}
after = 1 // Schedule on next tick, follows browser behavior
}
this._idleTimeout = after
this._idlePrev = this
this._idleNext = this
this._idleStart = null
// This must be set to null first to avoid function tracking
// on the hidden class, revisit in V8 versions after 6.2
this._onTimeout = nullv
this._onTimeout = callback
this._timerArgs = args
this._repeat = isRepeat ? after : null
this._destroyed = false
if (isRefed) incRefCount()
this[kRefed] = isRefed
initAsyncResource(this, 'Timeout')
}
复制代码
从构造函数的函数体可见,nodejs
中全部计时器是经过一个 双向链表 实现关联,而且全部超出时间阈值范围的时间阈值都会被 重置为 1ms,且全部非整数值会被转换为 整数值。
那么一种常见的写法 setTimeout(callback, 0)
会被 nodejs
内部模块转换为 setTimeout(callback, 1)
来执行。
pending callbacks
阶段用于执行先前事件循环 tick
中延迟执行的 I/O
回调函数。
poll
阶段的首要职责是:
计算因处理 I/O
须要阻塞当前事件循环 tick
的时间;该阻塞表示当前事件循环 tick
应该在当前 poll
阶段停留多久,这个时间通常是根据最小的 setTimeout/setInterval
的时间阈值等多个因素(见下文)来肯定。在到达阻塞时间后,会经历当前事件循环 tick
的后续阶段,并最终进入下一个事件循环 tick
的 timer
阶段,此时,过时的计时器的回调函数得以执行。
处理事件回调。
如前文概述,nodejs
中 poll
阶段对应 libuv
中的 核心逻辑 以下:
timeout = 0;
/** * uv_backend_timeout 用于获取 poll 阶段的超时(阻塞)时间 * http://docs.libuv.org/en/v1.x/loop.html#c.uv_backend_timeout */
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
复制代码
在调用 uv__io_poll 以前,首先初始化一个 timeout
变量,该变量在 loop
为常规模式下,将经过 uv_backend_timeout(loop)
定义 来肯定 poll
阶段的超时时间,该超时时间也就是 nodejs
文档 中提到的 poll
阶段应该阻塞的时间,那么肯定该阻塞时间的具体依据是什么呢?
int uv_backend_timeout(const uv_loop_t* loop) {
// https://github.com/libuv/libuv/blob/v1.35.0/src/uv-common.c#L521-L523
// http://docs.libuv.org/en/v1.x/guide/eventloops.html#stopping-an-event-loop
if (loop->stop_flag != 0)
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
复制代码
从 uv_backend_timeout
函数体不难看出,该函数根据当前事件循环 tick
的部分属性来肯定 poll
阶段的阻塞时间:
当事件循环 tick
不处于活动状态时且不存在活动的 request 时返回 0
,即不阻塞。
当 idle
句柄队列不为空时,返回 0
,即不阻塞。
当 pending callbacks 的回调队列不为空时,返回 0
,即不阻塞。
当存在 closing 句柄,即存在 close
事件回调时,返回 0
,即不阻塞。
为何返回 0
表示不阻塞,而 -1
表示无限制阻塞?
由于从 uv__io_poll 函数体可见 poll
阶段实现轮询的关键点在于各个系统平台的轮询机制。上文中 0
和 -1
分别对应 linux
系统底层轮询机制的轮询参数。
以 linux
的 epoll
轮询机制为例,在 uv__io_poll 函数体中调用了系统底层 epoll_wait 函数来实现 libuv
的轮询核心功能:
nfds = epoll_wait(loop->backend_fd,
events,
ARRAY_SIZE(events),
timeout);
复制代码
The parameter timeout shall specify the maximum number of milliseconds that epoll_wait() shall wait for events. If the value of this parameter is 0, then epoll_wait() shall return immediately, even if no events are available, in which case the return code shall be 0. If the value of timeout is -1, then epoll_wait() shall block until either a requested event occurs or the call is interrupted.
从 epoll_wait 文档可见,当 timeout
传参为 0
时,将当即返回,当 timeout
传参为 -1
时,将无限制阻塞,直到某个事件触发或无限阻塞状态被主动打断。
回到 timeout
的主题上,在不知足以上不阻塞当前事件循环 tick
的前提下,由 uv__next_timeout 函数来计算最终的 poll
阶段阻塞时间:
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
// libuv 计时器二叉最小堆的根节点为全部计时器中距离当前时间节点最近的计时器
heap_node = heap_min(timer_heap(loop));
// 此处 true 条件为无限制的阻塞当前 poll 阶段
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
// 若最近时间节点的计时器小于等于当前事件循环 `tick` 开始的时间节点
// 那么不阻塞,并进入下一阶段,直至进入下一 `tick` 的 `timer` 阶段执行回调函数
if (handle->timeout <= loop->time)
return 0;
// 如 nodejs 文档中对 poll 阶段计算阻塞时间的描述
// 如下语句用于计算当前 poll 阶段应该阻塞的时间
diff = handle->timeout - loop->time;
// INT_MAX 在 limits.h 头文件中声明
if (diff > INT_MAX)
diff = INT_MAX;
return (int) diff;
}
复制代码
从以上函数体并结合前文 对计时器的分析 不难看出,经过获取计时器最小堆的根节点获得距离如今最近的计时器执行节点。将该节点与当前事件循环 tick
的开始时间 loop->time
作对比:
若不存在任何计时器,那么当前事件循环 tick
中的 poll
阶段将 无限制阻塞。以实现一旦存在 I/O
回调函数加入到 poll queue
中便可当即获得执行。
若最近计时器时间节点小于等于开始时间,则代表在计时器二叉最小堆中 至少存在一个 过时的计时器,那么当前 poll
阶段的超时时间将被设置为 0
,即表示 poll
阶段不发生阻塞。这是为了尽量快的进入下一阶段,即尽量快地结束当前事件循环 tick
。在进入下一事件循环 tick
时,在 timer
阶段,上一 tick
中过时的计时器回调函数得以执行。
若最近计时器时间节点大于开始时间,则计算两个计时器以前的差值,且不大于 int
类型最大值。poll
将根据此差值来阻塞当前阶段,这么作是为了在轮询阶段,尽量快的处理异步 I/O
事件。此时咱们也能够理解为 事件循环 tick
始终有一种维持在 poll
阶段的倾向。
由以上源码分析,不可贵出 poll
阶段的本质:
为了尽量快的处理异步 I/O
事件,那么事件循环 tick
总有一种维持 poll
状态的倾向;
当前 poll
阶段应该维持(阻塞)多长时间是由 后续 tick
各个阶段是否存在不为空的回调函数队列 和 最近的计时器时间节点 决定。若全部队列为空且不存在任何计时器,那么事件循环将 无限制地维持在 poll
阶段。
注:由于 poll
阶段的超时时间在进入 poll
阶段以前计算,故当前 poll
阶段中回调函数队列中的计时器并不影响当前 poll
阶段的超时时间。
Note: Technically, the poll phase controls when timers are executed.
从技术上来讲,poll
阶段控制了计时器的执行时机。为何这么说?
首先,libuv
的事件循环是没法再入的,而且事件循环老是有一种维持在 poll
阶段的倾向,那么在没有知足 poll
阶段的结束条件时,就没法进入到下一个事件循环 tick
的 timer
阶段,就没法执行 timer queue
中到期计时器的回调函数。因此才会存在 “poll
阶段控制了计时器回调函数的执行时机” 的说法。
另外,无限制的轮询事件和调用回调函数,会致使彻底不会清空 poll
的回调函数队列,进而永远都不会发生计时器的阈值检测致使拖垮整个事件循环迭代。libuv
在其内部设定了一个依赖于系统的最大执行数。结合前文对 nodejs
内置计时器 的描述,这也是计时器没法保证准确的执行回调函数,而是尽快的执行回调函数的缘由之一。
该阶段的设计目的是可在 poll
阶段结束之时,当即调用指定代码片断(即函数)。若是 poll
阶段进入 idle
状态而且 setImmediate
函数存在回调函数时,那么 poll
阶段将打破无限制的等待状态,并进入 check
阶段执行 check
阶段的回调函数。
check
阶段的回调函数队列中全部的回调函数都是来自 poll
阶段的 setImmediate
函数。
由前文 nodejs 内置计时器 章节可知,在现行的 nodejs
环境中,有且仅有两种计时器,一种是 setTimeout/setInterval
,另外一种是 setImmediate
。
setTimeout/setInterval
设计目的在于经历一段最小时间阈值后尽快调用指定的回调函数。而 setImmediate
是做为特殊的计时器而存在,其设计目的是给予用户能在 poll
阶段结束后(即 check
阶段)可以当即执行代码的机会,而不用在 timer
阶段执行。
结合以上简短介绍,若同时在 user code
的模块词法环境中直接调用 setTimeout
和 setImmediate
会出现什么样的结果?
为何上文提到在
nodejs
中user script
是模块词法环境而不是全局词法环境?可简单经过
console.log(this === module.exports)
(而不是global
) 为true
值判断。
// index.js
setTimeout(
/* setTimeoutCallback */ () => {
console.log('from setTimeout')
},
0
)
setImmediate(
/* setImmediateCallback */ () => {
console.log('from setImmediate')
}
)
复制代码
以上代码经过 node index.js
命令调用后会出现 没法预测的随机 结果:
from setTimeout
from setImmediate
复制代码
或
from setImmediate
from setTimeout
复制代码
为何会出现这样的现象?
在 nodejs
脚本初始编译运行时,nodejs
会首先以入口 JS
文件为执行入口,那么此时 运行中执行上下文
为当前入口 JS
文件对应的 Script 执行上下文。
如前文所述,setTimeout(callback, 0)
实际上是被重置为 setTimeout(callback, 1)
了。那么在首次 user script
代码执行后,即 Script
执行上下文退出执行上下文栈后,并 开始首次 事件循环 tick
[nodejs 贡献者],在第一次进入 timer
阶段时,会抽取 timer
最小堆中的节点对比当前事件循环 tick
的开始时间是否已通过了阈值 1ms
:
若在前文 uv__run_timer(loop)
中,系统时间调用和时间比较的过程总耗时没有超过 1ms
的话,在 timer
阶段会发现没有过时的计时器,setTimeoutCallbacks
同时也并不存在于 timer queue
中。那么此时,将继续执行至 poll
阶段,而在 poll
阶段 poll queue
队列为空时,检查 check queue
队列并不为空。那么继续进入事件循环 tick
的下一阶段,并清空 check queue
中由 setImmediate
注册的 setImmediateCallback
回调函数。在经历后续的事件循环 tick
并从新开始时,会发现先前的阈值为 1ms
的过时计时器,此时的 setTimeoutCallback
才得以加入 timer queue
并得以在当前 timer
阶段执行。
控制台的输出以下:
from setImmediate
from setTimeout
复制代码
若在上文源码中,系统时间调用和时间比较的过程总耗时超过 1ms
的话,那么会将过时计时器的 setTimeoutCallback
加入到 timer queue
中,并进入 timer queue
的调用阶段。后续控制台输出以下:
from setTimeout
from setImmediate
复制代码
那么从上文针对 libuv
的 uv__run_timers 函数的分析可见,在 user script
的模块词法环境中直接同时调用 setTimeout(callback, 0)
和 setImmediate(callback)
时没法预判回调函数的调用顺序的缘由总结以下相关 issue:
在初始的事件循环 tick
执行时,会 首先执行第一次时间检查。
timer
句柄中 timeout
存储的是当次事件循环 tick
的开始时间加上 时间阈值
(示例代码中为 1ms
)后的时间节点。
这一次初始 timer
的时间检查距当前事件循环 tick
的间隔可能小于 1ms
也可能大于 1ms
的阈值,这取决于时间的系统调用的耗时,而时间的系统调用又会受到操做系统的其余应用的影响。当间隔小于 1ms
时,将在 timer
阶段忽略示例代码中的 setTimeoutCallback
执行,并先执行 setImmediateCallback
函数;反之,首先执行 setTimeoutCallback
执行。
nodejs
官网另外 描述 在 I/O cycle
中,示例代码的调用是可预测的,为何?
const fs = require('fs')
fs.readFile(__dirname, () => {
setTimeout(() => {
console.log('from setTimeout')
}, 1)
setImmediate(() => {
console.log('from setImmediate')
})
})
复制代码
上述示例代码将始终输出:
from setImmediate
from setTimeout
复制代码
The main advantage to using setImmediate() over setTimeout() is setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.
基于先前的分析,在经历初次事件循环 tick
后,后续全部的 setTimeout/setInterval
计时器阈值检查和调用都被先前事件循环 tick
的 poll
阶段所阻塞。而不论根据 nodejs
仍是 libuv
的事件循环抽象结构图仍是 uv_run 函数的源码,而且基于事件循环 没法再入 的前提,poll
阶段的下一阶段始终是 check
阶段,那么在 I/O cycle
中,全部的 timer
在当前事件循环 tick
中注册,并首先经过包含 setImmediate
回调函数的 check
阶段及其后续阶段,才会进入到下一事件循环 tick
的 timer
阶段。以致于在执行顺序上在 I/O cycle
中注册的 setTimeout/setInterval
回调函数始终在 setImmediate
的回调函数以后执行。以上一样说明了为何在 nodejs
官网上 描述 在 I/O cycle
中 setImmediate
的优先级高于 setTimeout
。
此阶段用于执行全部的 close
事件的回调函数。如忽然经过 socket.destroy()
关闭 socket
链接时,close
事件将在此阶段触发。
nodejs
与浏览器端的 Web API
版本的事件循环最大的不一样的是:
在 nodejs
中事件循环再也不是由单一个 task queue
和 micro-task queue
组成,而是由多个 阶段 phase
的多个回调函数队列 callbacks queues
组成一次事件循环 tick。 而且在每个单独的阶段都存在一个单独的 回调函数 FIFO 队列。