如何理解Node.js的事件循环

由于JavaScript是单线程的,那么在浏览器中,为了在等待动作完成时不会阻塞主线程的异步代码处理,JavaScript使用事件循环在调用堆栈、Web API和回调队列之间,持续协调代码的执行。不过,由Node.js自行实现的Node.js事件循环,虽然与之有着许多相同的模式,但是由于Node.js不与DOM交互,且可以处理各种输入和输出(I/O),因此它在工作方式上却有所不同。

在本文中,我们将先了解Node.js事件循环背后的理论,再探究几个使用setTimeout、setImmediate和process.nextTick的示例。最后,我们将部分工作代码部署到Heroku中,以查看其运行情况。

Node.js的事件循环

总的说来,Node.js事件循环可以协调计时器、回调、以及I/O事件等操作与执行。这便是Node.js在单线程的情况下,处理异步行为的方式。如下事件循环图,很好地展示了其执行的顺序。

如您所见,Node.js事件循环共有六个主要阶段,它们分别是:

计时器(Timers):那些由setTimeout和setInterval安排的回调,会在此阶段被执行。

待处理的回调(Pending callbacks):那些被推迟到下一个循环迭代的I/O回调,会在此阶段被执行。

空闲,准备(Idle, prepare):此阶段仅由Node.js内部所使用。

轮询(Poll):此阶段用于检索新的I/O事件,并执行I/O回调(不过那些由计时器和setImmediate安排的回调,以及下面将提到的关闭回调除外,毕竟它们会在其他不同的阶段被处理)。

检查(Check):由setImmediate安排的回调会在该阶段被执行。

关闭回调(Close callbacks):此阶段主要执行诸如销毁套接字连接等回调。

您可能会好奇,为何process.nextTick并未在上述任何阶段被提到?其实,这是因为:作为一种特殊的方法,就技术而言,它并非Node.js事件循环的一部分。相反,无论process.nextTick方法在何时被调用,它都会将自己的回调放入队列之中,然后“无论事件循环当前处于哪个阶段,都会在完成当前操作后,处理排队中的各种回调”(源自:Node.js事件循环文档)。

事件循环的场景示例

也许您觉得上文针对Node.js事件循环的每个阶段的解释,过于抽象了。那么,我在Heroku上创建了一个包含了各种可运行代码段示例的演示应用。在该应用中,单击任何示例按钮,都会向服务器端发送一个API请求。而Node.js会在后端执行所选示例的代码片段,然后通过API将相应的响应返回给前端。您可以从GitHub的链接处,查看到完整的代码。

让我们通过如下示例,来更好地理解Node.js事件循环的调用顺序。

示例1

让我们从如下简单的示例开始(如下图所示):

示例1-同步代码

在此,我们有三个功能函数。由于它们是同步的,因此代码会从上至下顺次执行。也就是说,如果三个函数的调用顺序为:first、second、third,它们的代码也会以相同的顺序去执行:first、second、third。

示例2

接下来,我们会在第二个示例中引入setTimeout的概念(如下图所示):

示例2-setTimeout

在此,我们先调用first函数,然后在延迟0毫秒后计划调用带有setTimeout的second函数,最后调用third函数。那么,这些函数的执行顺序就变成了:first、third、second。您一定会好奇:为什么second函数会被最后执行呢?

下面让我们来理解两个重要的原则。首先,使用带有延迟值的setTimeout方法,并不意味着应用将在指定毫秒数后,立即执行回调函数。实际上,该值表示的是:执行回调之前,需要经过的最短时间。其次,使用setTimeout来为回调设定的后期执行时间,会在事件循环的每一次迭代期间中始终执行该规则。因此,在事件循环的第一次迭代中,first函数被执行,second函数被“安排”(scheduled),third函数再被执行。然而,在事件循环的第二次迭代期间中,0毫秒的最小延迟已被满足,因此second函数便会在第二次迭代的“计时器”阶段被执行。

示例3

然后,我们会在第三个示例中引入setImmediate的概念(如下图所示):

示例3-setImmediate与setTimeout

在该示例中,我们执行first函数,使用setTimeout来为second函数延迟0毫秒,然后使用setImmediate来“安排”third函数。那么,在代码执行的过程中,就会出现一个问题:到底是哪种类型的安排优先?setTimeout还是setImmediate?

鉴于前面已经讨论过setTimeout的工作机制,我们来简单介绍一下setImmediate方法。该方法在事件循环的下一次迭代的“检查”阶段,会去执行其回调函数。因此,如果setImmediate在事件循环的第一次迭代期间被调用,那么它的回调方法会被“安排”上,并在事件循环的第二次迭代期间,执行该回调方法。

正如你在输出中所看到的那样,在我们的示例中,由于被setImmediate安排的回调先于被setTimeout安排的回调执行,因此该示例函数的执行顺序为:first、third、second。

当然,由setImmediate和setTimeout安排的执行到底谁先谁后,实际上取决于被调用方法的上下文。当从Node.js脚本中的主模块,直接调用这两种方法时,其时间取决于进程的性能,因此在每次运行脚本时,回调都可以按照不同的顺序被执行。不过,在I/O周期内调用这些方法时,setImmediate回调总是发生在setTimeout回调之前。在我们上述示例中,由于这些方法是作为响应API端点的某个部分被调用的,因此setImmediate回调会始终在setTimeout回调之前被执行。

示例4

为了实现快速的健全性检查,我们使用setImmediate和setTimeout来构建另一个示例(如下图所示)。

示例4-再次使用setImmediate与setTimeout

在此示例中,我们使用setImmediate来安排first函数,接着直接执行second函数,然后使用setTimeout的0毫秒延迟来安排third函数。您恐怕已经猜到了,上述函数的执行顺序为:second、first、third。而在事件循环的第二次迭代中,second函数被setImmediate安排在该I/O周期内被执行,然后third函数在延迟0毫秒时间后也被执行了。

示例5

下面,我们将process.nextTick方法引入最后一个示例(如下图所示)。

示例5-process.nextTick

在该示例中,我们使用setImmediate来安排first函数,并使用process.nextTick来安排second函数,再使用带有0毫秒延迟的setTimeout来安排third函数,最后执行fourth函数。那么,在代码运行后,整体的调用顺序为:fourth、second、first、third。

有了前面的基础,我们很容易理解fourth函数为何被首先执行了。毕竟它是被直接调用的,而无需通过任何其他方法来进行安排。process.nextTick方法安排了second函数在第二个被执行,first函数紧接其后。最后被执行的是third函数,其原因在于,在同一个I/O周期内,由setImmediate安排的回调会先于setTimeout安排的回调去执行。

那么,为什么由process.nextTick安排的second函数会先于由setImmediate安排的first函数被执行呢?请不要被这两种方法的名称所误导,并非setImmediate就代表着回调一定会被立即执行,而process.nextTick就一定要等到事件循环的下一轮再执行回调。您只需注意的是:process.nextTick是在安排的同一阶段中,立即执行回调的;而setImmediate的回调则是在事件循环的下一次迭代、或计时期间中被执行的。

小结

通过上述示例,您应该对Node.js的事件循环,以及诸如setTimeout、setImmediate和process.nextTick等方法有所了解了。当然,您不必深究Node.js的内部结构,以及处理命令的相关操作。我们完全可以将Node.js视为一个黑匣子,轻松地用好Node.js事件循环的各项调用顺序即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容