Node.js 事件驱动实现概览

发布于 2021-11-07 22:13:50 字数 4229 浏览 1437 评论 0

虽然在 ECMAScript 的标准里并没有(也没有必要)明确规定 事件,但是在浏览器中,事件作为一个极为重要的机制,给予 JavaScript 响应用户操作与 DOM 变化的能力;在Node.js中,异步事件驱动模型则是其高并发能力的基础。

学习JavaScript也需要了解它的运行平台,为了更好的理解JavaScript的事件模型,我打算从Node及浏览器引擎源码入手,分析其底层实现,并将我的分析整理为一系列博文;一方面作为笔记,另一方面也希望能与大家交流,分析和理解有疏漏偏颇之处,还望各位斧正。


简述事件驱动模型

解释 JavaScript 事件模型本身的好文章已经很多了,可以说这已经是一个说烂了的话题,这里我只简单写一下,并且提供一些好文章的链接。

程序如何响应事件

我们的程序响应外部的事件有如下两种方式:

  • 中断
    操作系统处理键盘等硬件输入就是通过中断来进行的,这个方式的好处是即使没有多线程,我们也可以放心地执行我们的代码,CPU收到中断信号之后自动地转去执行相应的中断处理程序,处理完成后会恢复原来的代码的执行环境继续执行。这种方式需要硬件的支持,一般来说都会被操作系统封装起来。
  • 轮询
    循环检测是否有事件发生,如果有就去执行相应的处理程序。这在底层和上层的开发中都有应用。
    Windows窗口程序就需要在主线程中写下如下代码,通常称做消息循环:
MSG msg = { };
while (GetMessage(&msg, NULL, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

消息循环不断检测是否有消息(用户的UI操作、系统消息等)出现,有的话就分发消息,调用相应的回调函数进行处理。

轮询方式的一个缺点就是:如果在主线程的消息循环里进行耗时操作,程序就无法及时响应新的消息。这在JavaScript中表现明显,以后还会提到这一点,并探讨其解决方案。

然而JavaScript中并没有类似消息循环代码,我们只是简单地注册事件,然后等待被调用。这是因为浏览器、Node作为执行平台,已经将event loop实现了,JavaScript代码不需要介入到这个过程中,只需要作为被调用者安静地等待即可。

相关讨论

  1. 知乎-关于浏览器处理事件的问题?匿名用户的回答:这个回答里图很不错,有助于理解 event loop 的工作原理;答案末尾有一些文章分享;
  2. MDN - Concurrency model and Event Loop:MDN 上对 event loop 的介绍。

Node 中的 event loop

通过 Node 源码看 event loop 的实现

Node 采用 V8 作为 JavaScript 的执行引擎,同时使用 libuv 实现事件驱动式异步 I/O。其事件循环就是采用了libuv的默认事件循环。

在 src/node.cc 中,

Environment* env = CreateEnvironment(
        node_isolate,
        uv_default_loop(),
        context,
        argc,
        argv,
        exec_argc,
        exec_argv);

这段代码建立了一个node执行环境,可以看到第三行的uv_default_loop(),这是libuv库中的一个函数,它会初始化uv库本身以及其中的default_loop_struct,并返回一个指向它的指针default_loop_ptr

之后,Node会载入执行环境并完成一些设置操作,然后启动event loop:

bool more;
do {
  more = uv_run(env->event_loop(), UV_RUN_ONCE);
  if (more == false) {
    EmitBeforeExit(env);
    // Emit `beforeExit` if the loop became alive either after emitting
    // event, or after running some callbacks.
    more = uv_loop_alive(env->event_loop());
    if (uv_run(env->event_loop(), UV_RUN_NOWAIT) != 0)
      more = true;
  }
} while (more == true);
code = EmitExit(env);
RunAtExit(env);
...

more用来标识是否进行下一轮循环。

env->event_loop()会返回之前保存在env中的default_loop_ptruv_run函数将以指定的UV_RUN_ONCE模式启动libuv的event loop。在这种模式下,uv_run会至少处理一个事件:这意味着,如果当前事件队列中没有需要处理的I/O事件,uv_run会阻塞住,直到有I/O事件需要处理,或者下一个定时器时间到。如果当前没有I/O事件也没有定时器事件,则uv_run返回false。

接下来Node会根据more的情况决定下一步操作:

  • 如果moretrue,则继续运行下一轮loop
  • 如果morefalse,说明已经没有等待处理的事件了,EmitBeforeExit(env);触发进程的'beforeExit'事件,检查并处理相应的处理函数,完成后直接跳出循环。

最后触发 'exit' 事件,执行相应的回调函数,Node运行结束,后面会进行一些资源释放操作。

在libuv中,event loop会在每次循环的开始更新自己的time从而实现计时功能,而I/O事件则分为两类:

  • Network I/O是使用系统提供的非阻塞式I/O解决方案,例如在Linux上使用epoll,windows上使用IOCP。
  • 文件操作和DNS操作没有(很好的)系统解决方案,因此libuv自建了线程池,在其中进行阻塞式I/O。

另外我们也可以将自定义的函数抛到线程池中运行,在运行结束后主线程会执行相应的回调函数,不过Node并没有将这一项功能加入到JavaScript中,也就是说只用原生Node是无法在JavaScript中开启新的线程进行并行执行的。

相关资料

  1. libuv Design Overview:关于 libuv 的架构及设计思路;
  2. node child_process 'exit':Node 的 child_process 'exit' 事件;
  3. Node with threads:讨论了使用 libuv 线程池异步运行 JavaScript 代码。

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

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

发布评论

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

关于作者

平定天下

暂无简介

0 文章
0 评论
573 人气
更多

推荐作者

醉城メ夜风

文章 0 评论 0

远昼

文章 0 评论 0

平生欢

文章 0 评论 0

微凉

文章 0 评论 0

Honwey

文章 0 评论 0

qq_ikhFfg

文章 0 评论 0

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