Node.js 中定时器的实现

发布于 2021-11-07 22:14:28 字数 3550 浏览 1387 评论 0

上一篇博文提到,在 Node 中 timer 并不是通过新开线程来实现的,而是直接在 event loop 中完成。下面通过几个 JavaScript 的定时器示例以及 Node 相关源码来分析在 Node 中,timer 功能到底是怎么实现的。

JavaScript 中定时器功能的特点

无论是 Node 还是浏览器中,都有 setTimeout 和 setInterval 这两个定时器函数,并且其工作特点基本相同,因此下面仅以Node为例进行分析。

我们知道,JavaScript 中的定时器并不同于计算机底层的定时中断。中断到来时,当前执行代码会被打断,转去执行定时中断处理函数。而JavaScript的定时器到时,如果当前执行线程没有正在执行的代码,则执行相应的回调函数;如果当前有代码在执行中,JavaScript引擎既不会中断当前代码转去执行回调,也不会开新的线程执行回调,而是当前代码执行完毕之后才去处理。

console.time('A')
setTimeout(function () {
    console.timeEnd('A');
}, 100);
var i = 0;
for (; i < 100000; i++) { }

执行上面的代码,可以看到最终输出的时间并不是100ms左右,而是数秒。这说明在循环完成之前,定时回调函数确实没有被执行,而是推迟到了循环结束。实际上在JavaScript代码执行中,所有的事件都无法得到处理,必须等到当前代码全部完成,才能去处理新的事件。这就是为什么在浏览器中运行耗时JavaScript代码时,浏览器会失去响应。为了应对这种情况,我们可以采取Yielding Processes的技巧,将耗时的代码分成小块(chunks),每处理完一块就执行一次setTimeout,约定在一小段时间后才处理下一块,而在这段空闲时间里,浏览器/Node可以去处理排队中的事件。

补充资料

JavaScript 高级程序设计 第三版 第22章 高级技巧 中对高级定时器以及 Yielding Processes 有较详细的讨论。

Node 中的 timer 实现

libuv 对 uv_loop_t 类型的初始化

Node 会调用 libuv 的 uv_run 函数启动 default_loop_ptr 进行事件调度,default_loop_ptr 指向一个uv_loop_t类型的变量default_loop_struct。Node启动时会调用uv_loop_init(&default_loop_struct)对其进行初始化,uv_loop_init函数节选如下:

int uv_loop_init(uv_loop_t* loop) {
  ...
  loop->time = 0;
  uv_update_time(loop);
  ...
}

可以看到looptime字段先被赋值为0,之后调用uv_update_time函数,这会将最新的计数时间赋给loop.time

初始化完成之后,default_loop_struct.time就有了一个初始值,与时间有关的操作都会与此值进行比较从而确定是否调用相应回调函数。

libuv 的事件调度核心

前面提到uv_run函数就是libuv库实现event loop的核心部分,下面是其流程图:

uv_run

这里简述一下上面与定时器相关的逻辑:

  1. 更新当前looptime字段,这个字段标志着当前loop概念下的“现在”;
  2. 检查loop是否alive,也就是说检查loop中是否还有需要处理的任务(handlers/requests),如果没有就不必循环了;
  3. 检查注册过的timer,如果某一个timer中指定的时间落后于当前时间了,说明该timer已到时,于是执行其对应的回调函数;
  4. 执行一次I/O polling(即阻塞住线程,等待I/O事件发生),如果在下一个timer到期时还没有任何I/O完成,则停止等待,执行下一个timer的回调。
    如果发生了I/O事件,则执行对应的回调;由于执行回调的时间里可能又有timer到期了,这里要再次检查timer并执行回调。
    (实际上(4.)这里比较复杂,不仅仅是一步操作,这样描述仅是为了不涉及其他细节,而专注于timer的实现。)

Node会一直调用uv_run直到loop不再alive。

Node 中的 timer_wrap 与 timers

Node中有一个TimerWrap类,被注册为Node内部的timer_wrap模块。

NODE_MODULE_CONTEXT_AWARE_BUILTIN(timer_wrap, node::TimerWrap::Initialize)

其中TimerWrap类基本上就是对 uv_timer_t 的一个直接封装,NODE_MODULE_CONTEXT_AWARE_BUILTIN 是Node用于注册built-in模块的宏。

经过这一步操作,JavaScript就可以拿到这个模块进行操作了。src/lib/timers.js 文件使用JavaScript的形式把timer_wrap的功能封装起来,并导出了 exports.setTimeout, exports.setInterval, exports.setImmediate 等函数。

Node 启动与 global 初始化

上一篇提到 Node 启动时会载入执行环境 LoadEnvironment(env),这个函数中非常重要的一步就是载入 src/node.js并执行,src/node.js会载入指定的模块并初始化globalprocess。当然,setTimeout 等函数也会被 src/node.js 绑定到 global 对象上。

至此,setTimeout/setInterval这类定时器函数已经可以为JavaScript所用了。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

浅沫记忆

暂无简介

0 文章
0 评论
764 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文