前言

如果你了解过Node.js,那么你一定听说过事件循环。你一定想知道它为什么那么特殊,并且为什么你需要关注它?此时此刻的你,可能已经写过许多基于Express.js的后端代码,但没有接触到任何的循环。

在下文中,我们会先在一个更高的,无关操作系统的层面上了解事件循环,然后再去深入到Node.js中观察它。

事件和事件处理器

在事件循环里,有两个主要角色:

  • 事件
  • 事件处理器,即这些事件的订阅者

事件,可以是十分底层的操作系统事件,如“文件已经准备好被写入”或“收到了一个新的HTTP请求”。
事件处理器,则是当指定事件触发时,执行的一段代码。

事件循环中,事件的获取和事件处理器的执行

eventloop

事件循环的职责,就是不断得等待事件的发生,然后将这个事件的所有处理器,以它们订阅这个事件的时间顺序,依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。

当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件处理器是一个一个按顺序执行的。

即如果某个事件绑定了两个处理器,那么第二个处理器会在第一个处理器执行完毕后,才开始执行。在这个事件的所有处理器都执行完毕之前,事件循环不会去检查是否有新的事件触发。在单个线程中,一切都是有顺序地一个一个地执行的!

在事件处理器的执行代码中触发了事件

一个有趣而且常会出现的情况是,在执行一个事件处理器的代码里,代码触发了另一个事件。例如,在文件可以被读之后,这个事件的处理器开始读取内容,期间处理器又触发了一个写事件,来将这个文件中已读取的这部分内容响应给正在处理的HTTP请求。写入完毕之后,继续读取文件。这就是事件循环保持运作的方式。

事件被触发,然后以订阅顺序执行处理器,不断往复。这个循环圈就是事件循环控制流的关键 ,在没有更多的订阅事件的处理器之后,Node.js就会退出。

操作系统的帮助

事件在实质上是从哪里来?事件循环会不断获取下一个被触发的事件,这是如何发生的?你是对的,这需要操作系统的帮助。幸运的是,现代操作系统中有许多方式可以实现这些(selectepollkqueueIOCP)。在日常使用时,通常会在操作系统提供的这些方式上会再抽象出一层(在Node.js中,就是libuv)。

另一个需要操作系统帮助的,就是事件的订阅,如注册在特定的事件发生时需要执行的代码。这也是事件循环中必须要实现的。

Node.js中的事件循环

事件循环是Node.js中非常核心的组成部分,许多Node.js的特性都依赖于它,它既有积极的影响也不好的影响。比如在处理I/O密集任务时的性能提升和缺乏足够信息量的错误堆栈信息。Node.js异步回调驱动的编程范式,便直接是源于事件循环的存在。

每一个Node.js进程中都存在一个事件循环。只要进程存在,它就存在,一直不间断地调度执行着你程序中的方法和操作系统方法。事件循环以一个无限循环的形式启动,存在于Node.js二进制文件里main函数的最后,当没有更多可被执行的事件处理器时,它就退出。它运行于单个线程中,并且事件处理器是一个接一个顺序执行的。