Node.js事件循环

事件循环是了解节点最重要的方面之一

介绍

事件循环是有关Node的最重要方面之一。

为什么这个这么重要?因为它说明了Node如何实现异步并具有无阻塞的I / O,所以它基本上说明了Node的“杀手级应用”,这是使它成功的原因。

Node.js JavaScript代码在单个线程上运行。一次只发生一件事。

这个限制实际上非常有用,因为它大大简化了您的编程方式,而无需担心并发问题。

您只需要注意如何编写代码,并避免任何可能阻塞线程的事情,例如同步网络调用或无限循环

通常,在大多数浏览器中,每个浏览器选项卡都有一个事件循环,以使每个进程都隔离开,并避免出现无限循环或繁重的处理过程的网页来阻塞您的整个浏览器。

该环境管理多个并发事件循环,以处理例如API调用。网络工作者也可以在自己的事件循环中运行。

您主要需要担心的是您的代码将在单个事件循环上运行,并且在编写代码时要牢记这一点,以避免阻塞它。

阻止事件循环

任何花费很长时间才能将控制权返回到事件循环的JavaScript代码,都将阻止页面中任何JavaScript代码的执行,甚至阻止UI线程,并且用户无法单击浏览,滚动页面等。

JavaScript中几乎所有的I / O原语都是非阻塞的。网络请求,文件系统操作等。被阻塞是一个例外,这就是为什么JavaScript如此多地基于回调,最近才基于诺言异步/等待

调用堆栈

调用堆栈是一个LIFO队列(后进先出)。

事件循环不断检查调用堆栈看看是否有任何需要运行的功能。

这样做时,它将找到的所有函数调用添加到调用堆栈中,并按顺序执行每个函数。

您知道在调试器或浏览器控制台中可能熟悉的错误堆栈跟踪吗?浏览器在调用堆栈中查找函数名称,以通知您哪个函数发起了当前调用:

Exception call stack

一个简单的事件循环说明

让我们举个例子:

我用foobarbaz作为随机名称。输入任何名称以替换它们。

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) bar() baz() }

foo()

此代码打印

foo
bar
baz

如预期的那样。

当此代码运行时,首先foo()叫做。里面foo()我们先打电话bar(),然后我们致电baz()

此时,调用堆栈如下所示:

Call stack first example

每次迭代中的事件循环都会检查调用堆栈中是否有东西,并执行它:

Execution order first example

直到调用堆栈为空。

排队功能执行

上面的示例看起来很正常,没有什么特别的:JavaScript查找要执行的事物,并按顺序运行它们。

让我们看看如何将函数推迟到堆栈清除之前。

用例setTimeout(() => {}), 0)就是调用一个函数,但是一旦代码中的每个其他函数都执行了,就执行它。

举个例子:

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) setTimeout(bar, 0) baz() }

foo()

这段代码打印出来,也许令人惊讶:

foo
baz
bar

运行此代码时,将首先调用foo()。在foo()内部,我们首先调用setTimeout,bar作为参数,我们指示它尽可能快地运行,并传递0作为计时器。然后我们调用baz()。

此时,调用堆栈如下所示:

Call stack second example

这是程序中所有功能的执行顺序:

Execution order second example

为什么会这样呢?

消息队列

调用setTimeout()时,Browser或Node.js将启动计时器。计时器到期后,在这种情况下,我们立即将超时值设为0,则将回调函数放入消息队列

消息队列也是用户启动的事件,例如单击或键盘事件,或者拿来在您的代码有机会对它们进行响应之前,对响应进行排队。或者也DOM像这样的事件onLoad

循环将优先级赋予调用堆栈,它首先处理在调用堆栈中找到的所有内容,一旦其中没有任何内容,便开始处理消息队列中的内容。

我们不必等待诸如setTimeout,抓取或其他方式来完成自己的工作,因为它们是由浏览器提供的,并且它们位于自己的线程中。例如,如果您设置了setTimeout超时为2秒,您不必等待2秒-等待发生在其他地方。

ES6作业队列

ECMAScript 2015引入了作业队列的概念,Promises使用了该队列(也在ES6 / ES2015中引入)。这是一种尽快执行异步函数结果的方法,而不是放在调用堆栈的末尾。

在当前功能结束之前解决的承诺将在当前功能之后立即执行。

我发现在游乐园中过山车的类比很好:消息队列将您排在队列的后面,在所有其他人的后面,您将不得不等待转弯,而工作队列是快速通道票这样您就可以在完成上一个骑行之后立即骑另一个骑行。

例子:

const bar = () => console.log('bar')

const baz = () => console.log(‘baz’)

const foo = () => { console.log(‘foo’) setTimeout(bar, 0) new Promise((resolve, reject) => resolve(‘should be right after baz, before bar’) ).then(resolve => console.log(resolve)) baz() }

foo()

此打印

foo
baz
should be right after baz, before bar
bar

这是Promises(和基于Promise构建的Async / await)与通过setTimeout()或其他平台API。

结论

本文向您介绍了Node.js事件循环的基本构建块。

这是用Node.js编写的任何程序的重要组成部分,我希望这里介绍的某些概念将来对您有用。

免费下载我的Node.js手册


更多节点教程: