
*图片来源:Tine Ivanič*on潇湘晨报
在浏览器中,事件循环协调调用栈、网络API和回调队列之间的代码执行。然而,Node.js实现了自己的"Node.js事件循环",这与常规的 "JavaScript事件循环 "不同。多么令人困惑啊
Node.js事件循环遵循许多与JavaScript事件循环相同的模式,但工作方式略有不同,因为它不与DOM交互,但会处理输入和输出(I/O)等事项。
在这篇文章中,我们将深入研究Node.js事件循环背后的理论,然后使用setTimeout 、setImmediate 和process.nextTick 看几个例子。我们甚至会将一些工作代码部署到Heroku(一种快速部署应用程序的简单方法),看看它的运作情况。
Node.js的事件循环协调来自计时器、回调和I/O事件的操作的执行。这就是Node.js处理异步行为的方式,同时仍然是单线程的。让我们看看下面的事件循环图,以更好地了解操作的顺序。
Node.js事件循环的操作顺序(来源:Node.js文档)
正如你所看到的,Node.js事件循环有六个主要阶段。让我们简单地看看每个阶段会发生什么。
- 计时器:由
setTimeout和setInterval安排的回调在这个阶段执行 - 挂起的回调。之前被推迟到下一次循环迭代的I/O回调在此阶段被执行
- 闲置、准备:该阶段仅由Node.js内部使用。
- 轮询:新的I/O事件被检索,I/O回调在此阶段被执行(由计时器安排的回调、由
setImmediate安排的回调以及关闭回调除外,因为这些都在不同的阶段处理) - 检查:由
setImmediate安排的回调在这个阶段执行。 - 关闭回调:关闭回调,如当套接字连接被破坏时,在此阶段执行。
值得注意的是,process.nextTick 在这些阶段中都没有提到。这是因为它是一个特殊的方法,在技术上不是Node.js事件循环的一部分。相反,每当调用process.nextTick 方法时,它都会将其回调放入队列中,然后这些队列中的回调会 "在当前操作完成后被处理,而不考虑事件循环的当前阶段"(来源:Node.js事件循环文档)。
事件环路示例场景
现在,如果你像我一样,这些对Node.js事件循环每个阶段的解释可能仍然显得有点抽象。我通过观察和实践来学习,所以我在Heroku上创建了这个演示应用程序,用于运行各种代码片段的例子。在这个应用程序中,点击任何一个例子的按钮都会向服务器发送一个API请求。然后,所选例子的代码片段由后端的Node.js执行,并通过API将响应返回给前台。你可以在GitHub上查看完整的代码。

Node.js事件循环演示应用程序
让我们看看一些例子,以便更好地理解Node.js事件循环的操作顺序。
例子1
我们将从一个简单的开始。
例1--同步代码
这里我们有三个同步函数,一个接一个地被调用。因为这些函数都是同步的,代码只是从上到下执行。因此,因为我们按照first,second,third 的顺序调用我们的函数,所以这些函数也是按照同样的顺序执行。first,second,third 。
例2
接下来,我们将通过第二个例子介绍setTimeout 的概念。
例2 - setTimeout
在这里,我们先调用我们的first 函数,然后用setTimeout 来安排我们的second 函数,延迟时间为0毫秒,然后调用我们的third 函数。这些函数是按照这个顺序执行的。first,third,second 。为什么会这样呢?为什么最后执行的是second 函数?
这里有几个关键的原则需要理解。第一个原则是,使用setTimeout 方法并提供一个延迟值,并不意味着回调函数将在这个毫秒数之后被准确执行。相反,这个值代表了在回调函数被执行之前需要经过的最小时间量。
要理解的第二个关键原则是,使用setTimeout ,将回调安排在稍后的时间执行,这将总是至少在事件循环的下一次迭代中。所以在这个事件循环的第一次迭代中,first 函数被执行,second 函数被安排,third 函数被执行。然后,在事件循环的第二次迭代中,已经达到了0毫秒的最小延迟,所以在第二次迭代的 "定时器 "阶段执行了second 。
例三
接下来,我们将通过第三个例子介绍setImmediate 的概念。
例3 - setImmediate vs. setTimeout
在这个例子中,我们执行了first 函数,用setTimeout 来安排我们的second 函数,延迟为0毫秒,然后用setImmediate 来安排我们的third 函数。这个例子引出了一个问题。在这种情况下,哪种调度方式优先?setTimeout 还是setImmediate ?
我们已经讨论了setTimeout 的工作方式,所以我们应该简单介绍一下setImmediate 方法的背景。setImmediate 方法在事件循环的下一次迭代的 "检查 "阶段执行其回调函数。因此,如果setImmediate 在事件循环的第一次迭代中被调用,它的回调方法将被安排,然后将在事件循环的第二次迭代中被执行。
正如你从输出中看到的,这个例子中的函数是按照这个顺序执行的。first,third,second 。所以在我们的例子中,由setImmediate 预定的回调在由setTimeout 预定的回调之前被执行。
值得注意的是,你看到的setImmediate 和setTimeout 的行为可能会有所不同,这取决于调用这些方法的环境。当这些方法被直接从Node.js脚本的主模块中调用时,时间取决于进程的性能,所以回调实际上可以在你每次运行脚本时以任一顺序执行。然而,当这些方法在一个I/O周期内被调用时,setImmediate 回调总是在setTimeout 回调之前被调用。因为在我们的例子中,我们是作为API端点响应的一部分来调用这些方法的,所以我们的setImmediate 回调总是在我们的setTimeout 回调之前被执行。
例四
作为一个快速的理智检查,让我们再运行一个使用setImmediate 和setTimeout 的例子。
例子4--再次使用setImmediate和setTimeout
在这个例子中,我们使用setImmediate 来安排我们的first 函数,执行我们的second 函数,然后使用setTimeout 来安排我们的third 函数,延迟为0毫秒。正如你可能已经猜到的,这些函数是按照这个顺序执行的。second,first,third 。这是因为first 函数被调度,second 函数被立即执行,然后third 函数被调度。在事件循环的第二次迭代中,second 函数被执行,因为它是由setImmediate 安排的,而且我们处于一个I/O周期中,然后third 函数被执行,因为我们处于事件循环的第二次迭代中,而且指定的0毫秒延迟已经过去。
你开始明白了吗?
例子5
让我们看一下最后一个例子。这一次我们将引入另一个方法,叫做process.nextTick 。
例5 - process.nextTick
在这个例子中,我们用setImmediate 来安排我们的first 函数,用process.nextTick 来安排我们的second 函数,用setTimeout 来安排我们的third 函数,延迟为0毫秒,然后执行我们的fourth 函数。这些函数最终按以下顺序被调用。fourth,second,first,third 。
fourth 函数首先被执行的事实不应该是一个惊喜。这个函数被直接调用,而没有被我们其他的方法安排。second 函数第二个被执行。这是一个与process.nextTick 一起被调度的函数。first 函数在第三位被执行,接着是third 函数在最后,这对我们来说也不应该感到惊讶,因为我们已经知道,在一个I/O周期内,由setImmediate 调度的回调会在由setTimeout 调度的回调之前被执行。
那么,为什么process.nextTick 安排的second 函数会在setImmediate 安排的first 函数之前被执行呢? 这里的方法名称有误导性。你会认为来自setImmediate 的回调会被立即执行,而来自process.nextTick 的回调会在事件循环的下一个tick中被执行。然而,实际上情况恰恰相反。令人困惑,对吗?
事实证明,来自process.nextTick 的回调会在它被安排的同一阶段立即执行。来自setImmediate 的回调会在事件循环的下一次迭代或勾选中执行。因此,在我们的例子中,由process.nextTick 预定的second 函数在由setImmediate 预定的first 函数之前被执行是合理的。
结论
现在你应该对Node.js的事件循环以及像setTimeout 、setImmediate 和process.nextTick 这样的方法更加熟悉了。你当然可以不深入了解Node.js的内部结构以及处理命令的操作顺序。然而,当你开始了解Node.js的事件循环时,Node.js就不再是一个黑盒子了。
如果你想再看看这些例子的实战情况,你可以随时查看演示应用程序或在GitHub上查看代码。 你甚至可以通过点击这里自己将代码部署到Heroku。
谢谢你的阅读!
主题。
javascript,node, nodejs, 事件循环, 事件循环阶段, Web开发, heroku