事件循环是 NodeJS 处理非阻塞 I/O 操作的和核心机制。NodeJS 的事件循环脱胎于 libuv 的事件循环,因此,要搞清楚 NodeJS 的事件循环,还需要先了解 libuv 的事件循环是如何工作的。

libuv 的事件循环

我们先来了解两个基本概念:句柄(handle)和请求(request).

  • 句柄是指在整个事件循环活跃时间内能够执行某些操作的长期对象。比如一个 TCP 服务句柄,每当有新的联接建立时,这个句柄的 connected 回调就会被调用。
  • 请求是通常指短期操作。比如向某个句柄中写入数据的操作。

了解了这两个概念以后,我们来看看 libuv 的事件循环是如何工作的。

下面这张图可以清楚的展示事件循环的执行过程:

结合这张图我们简单描述一下一次循环过程中各个步骤做了什么。

  1. 首先更新循环内的当前时间(now),避免在循环过程中多次发生与时间相关的系统调用。
  2. 检查当前事件循环是否还是活跃(active)的。检查的表示是当前事件循环是否还有活跃的句柄、活跃的请求操作,或者还有“关闭”回调的话,就视为是活跃的。如果判断当前循环不是活跃的,则直接退出。
  3. 执行所有的到期回调。即所有的到期时间在循环当前时间之前的回调都会被执行。
  4. 执行所有的挂起回调(pending callbacks)。所谓挂起回调,就是在上一个循环周期中设置的到下一循环周期在执行的回调。
  5. 执行空闲句柄回调(idle handle callbacks)。虽然名字中包含空闲二字,实际上每个循环周期都会执行。
  6. 执行准备句柄回调(prepare handle callbacks)。
  7. 在这一步会暂停循环,轮询等待 I/O 事件一段时间。这个时间长度是根据一个算法算出,这里不做详细说明。在轮询期间,所有 I/O 相关的回调会被执行(前提是系统通知到 libuv)。
  8. 执行检查句柄回调(check handle callbacks)。检查句柄回调往往与准备句柄回调相对应。这两个回调可以方便我们在 I/O 之前做一些准备工作,然后在 I/O 之后做相应的检查。
  9. 执行关闭回调(close callbacks)。比如通过 uv_close() 设置的回调。

整个事件循环就是 1 - 9 的循环执行。

值得说明的是,libuv 会在轮询阶段中断事件循环,等待系统通知。比如某个文件 I/O 已经完成,或者接收到一个网络连接等。在接收到系统通知后,事件循环会调用相关的回调执行操作。

不同的平台(windows\linux 等),异步 I/O 的机制不同,libuv 底层会根据不同平台,采用不同的 I/O 轮询机制,比如 epoll(linux)、kqueue(OSX)、IOCP(windows)等,上层不需要关注异步 I/O 的实现机制。

NodeJS 的事件循环

现在我们来看 NodeJS 的事件循环。同样,我们放一张 NodeJS 事件循环的过程图。

在 NodeJS 中,事件循环的每一步成为一个阶段,每个阶段都有一个 FIFO 队列来执行回调。通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列清空或达到最大回调数限制。当队列清空或者达到最大限制,事件循环进入下一阶段。

对比两个事件循环的图,我们可以看到,具体过程基本相同。因此,NodeJS 的事件循环过程我们简述如下:

  1. 定时器阶段,执行已经被 setTimeout()setInterval() 调度的回调函数。
  2. 挂起的回调,执行(在上一个循环中被设置)延迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare 阶段,仅 NodeJS 系统内部使用。
  4. 轮询阶段,检索新的 I/O 事件,执行与 I/O 相关的回调。与 libuv 一样,NodeJS 还在这个阶段暂停循环一段时间。
  5. 检测阶段,执行被 setImmediate() 调度的回调函数。
  6. 关闭的回调函数,执行一些关闭的回调函数,如:socket.on('close', ...)

我们对轮询阶段做个详细说明。

轮询阶段有两个重要的功能:

  • 计算应该阻塞和轮询 I/O 的时间。
  • 处理轮询队列里的事件。

一旦事件循环进入轮询阶段并且没有到期的定时器回调时,事件循环将做如下判断:

  • 如果轮询队列不是空的,那么事件循环将循环访问回调队列并同步执行它们,直到清空队列,或者达到了最大限制。
  • 如果轮询队列是空的,则再做如下判断:
    • 如果有代码是被 setImmediate() 调度的,那么事件循环将结束轮询阶段,并到检查阶段以执行那些被调度的代码。
    • 如果没有代码被 setImmediate() 调度,那么事件循环将等待回调被添加到队列中,然后立即执行。

在轮询阶段的执行过程中,一旦轮询队列为空,事件循环将检查是否有到期的定制器。如果一个或多个定时器已准备就绪,则事件循环将绕回定时器阶段以执行这些定时器的回调。

这里要特别对 setImmediate() 进行一些说明。

在 libuv 的事件循环中,允许开发人员在轮询阶段之前做些准备操作,然后在轮询阶段之后立即对这些操作进行检查。NodeJS 中 setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊定时器。它使用一个 libuv API 来安排回调在轮询阶段完成后执行。

setImmediatesetTimeoutprocess.nextTick

  • setImmediate() 被设计为一旦在当前轮询阶段完成,就执行代码。
  • setTimeout() 是在最小阈值(ms 单位)过后执行代码。
  • process.nextTick() 严格意义上讲并不属于事件循环的一部分。它不管事件循环的当前阶段如何,它都将在当前操作完成后处理 nextTickQueue 中排队的代码。

setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

我们看下面这段代码:

setTimeout(() => {
console.log('timeout');
}, 0); setImmediate(() => {
console.log('immediate');
});

这两个函数调用都在主模块中被调用,则他们的回调执行顺序是不定的,受进程的性能影响很大(进程会受到系统中运行其他应用程序影响)。

但是一旦将这两个函数放到 I/O 轮询调用内,那么 setImmediate() 一定会在 setTimeout() 之前被执行,不管有多个定制器已经到期。比如下面这段代码,总是会先输出 "immediate"。

const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

process.nextTick()setImmediate() 严格意义上来说,应该将名称互换。因为 process.nextTick()setImmediate() 触发得更快。

任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。之所以这么设计,是考虑到这些使用场景:

  • 允许开发者处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
  • 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

比如下面这段代码:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

只有传递端口时,端口才会立即被绑定,然后立即调用 'listening' 回调。问题是 .on('listening') 的回调在那个时间点尚未被设置。

为了绕过这个问题,'listening' 事件被排在 nextTick() 中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。

Promise

这里在补充说明一下 NodeJS 中 Promise 是如何处理的。我们之前说过,在浏览器的事件循环里,会有一个微任务的队列来防止所有的微任务,并且在每个操作之后,都尝试清空微任务队列。

在 NodeJS 中,做法类似,NodeJS 的事件循环中也有一个微任务队列,工作机制与 process.nextTick() 类似,在每个操作之后,事件循环都会尝试清空微任务队列。

总结

我们结合 libuv 的事件循环,详细说明了 NodeJS 事件循环的每一阶段的具体职能。同时,我们还分析了常用的几个异步代码函数的原理。

我们用一张图归纳如下:

常见面试知识点、技术解决方案、教程,都可以扫码关注公众号“众里千寻”获取,或者来这里 https://everfind.github.io

NodeJS 中的事件循环,读了这篇就全懂了的更多相关文章

  1. 深入理解javascript中的事件循环event-loop

    前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...

  2. nodeJS中的事件机制

    events模块是node的核心模块,几乎所有常用的node模块都继承了events模块,比如http.fs等.本文将详细介绍nodeJS中的事件机制 EventEmitter 多数 Node.js ...

  3. JavaScript中的事件循环机制跟函数柯里化

    一.事件循环机制的理解 test();//按秒输出5个5 function test() { for (var i = 0; i < 5; i++) { setTimeout(() => ...

  4. 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick

    [摘要] 官网博文翻译,nodejs中的定时器 示例代码托管在:http://www.github.com/dashnowords/blogs 原文地址:https://nodejs.org/en/d ...

  5. 【nodejs原理&源码赏析(7)】【译】Node.js中的事件循环,定时器和process.nextTick

    目录 Event Loop 是什么? Event Loop 基本解释 事件循环阶段概览 事件循环细节 timers pending callbacks poll阶段 check close callb ...

  6. TMsgThread, TCommThread -- 在delphi线程中实现消息循环(105篇博客,好多研究消息的文章)

    在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使用很方便,但是有时候我们需要在线程类中使用消息循环,delphi没有提供.   花了两天的事件研究了 ...

  7. nodejs笔记之事件循环

    Event  Loop  (事件循环或者事件轮询) Event Loop是一个程序结构,用于等待和发送消息和事件. 简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程&q ...

  8. JavaScipt 中的事件循环(event loop),以及微任务 和宏任务的概念

    说事件循环(event loop)之前先要搞清楚几个问题. 1. js为什么是单线程的? 试想一下,如果js不是单线程的,同时有两个方法作用dom,一个删除,一个修改,那么这时候浏览器该听谁的?   ...

  9. JavaScript中的事件循环

    JavaScript是单线程单并发语言 单线程:主程序只有一个线程,即同一时间片段内其只能执行单个任务. 引发的问题: 单线程,意味着任务都需要排队,前一个任务结束,才会执行后一个任务.若前一个任务耗 ...

随机推荐

  1. LeetCode解题记录(贪心算法)(一)

    1. 前言 目前得到一本不错的算法书籍,页数不多,挺符合我的需要,于是正好借这个机会来好好的系统的刷一下算法题,一来呢,是可以给部分同学提供解题思路,和一些自己的思考,二来呢,我也可以在需要复习的时候 ...

  2. Web 前端开发规范手册

    一.规范目的 Web 前端开发规范手册 1.1 概述 ......................................................................... ...

  3. 【知识点】inline函数、回调函数、普通函数

    目录 一.inline内联函数 1.1 使用 1.2 编译器对 inline 函数处理步骤 1.3 优缺点 1.3.1 优点 1.3.2 慎用内联 1.3.3 不宜使用内联 1.4 虚函数(virtu ...

  4. Tars | Win10下Docker部署TarsJava(SpringBoot)全过程及踩坑记录

    @ 目录 前言 1. 相关环境版本: 坑点一:VMware与Win10 Docker冲突 坑点二:20.版本TarsJava(SpringBoot)依赖文件缺失 2. Docker安装: 坑点三:Do ...

  5. 在docker中使用nginx部署前端项目

    前言 部署了三个nginx用于前端项目, 并使用keepalived部署好热备, 所以总共有5个nginx 创建好nginx的文件和配置 根据上面的指令创建好目录 mkdir /home/web/ng ...

  6. javascript数组 (转)

      javascript的Array可以包含任意数据类型,并通过索引来访问每个元素.   要取得Array的长度,直接访问length属性:   var arr = [1,2,3.14,'Hell0' ...

  7. IDA Pro 6.0使用Qt 框架实现了跨平台的UI

    IDA Pro 6.0使用Qt 框架实现了跨平台的UI.它的好处是插件编写者还可以直接使用 Qt 开发跨平台 UI.但是编剧呢? 在这篇博文中,我们将说明如何使用PySide使用IDAPython为 ...

  8. postgresql安装及配置

    目录 1. 安装 2. PostgrepSQL的简单配置 2.1 修改监听的ip和端口 2.2 修改数据库log相关的参数 2.3 内存参数 3. 数据库的基础操作 3.1 连接数据库控制台 3.2 ...

  9. 题解 guP3956 棋盘

    好吧本来这题可以用最短路跑完的,结果我硬是打了1.5小时的dfs... 其实这题并没有那么难,构造一个无向图再跑最短路即可. 我用的dj跑最短路 问题来了 如果(n,n)是无色的,那么图上就没有这个点 ...

  10. XDFZOI 月赛 201905 Sliver

    组题人自己组完过后,才发现自己还是太弱了... T1 简单模拟. 按照游戏规则直接模拟显然是不明智的,所以我们可以像石头剪刀布一样,将判断改变为检验. 同时,我们发现,一共只有48种牌,所以我们可以直 ...