绪论
事件循环是了解Node的最重要的方面之一。
为什么它如此重要?因为它解释了Node如何实现异步和非阻塞I/O,所以它基本上解释了Node的 "杀手级应用",也就是使它如此成功的东西。
Node.js的JavaScript代码在一个单线程上运行。每次只有一件事在发生。
这是一个实际上非常有帮助的限制,因为它简化了很多你如何编程而不用担心并发问题。
你只需要注意如何写你的代码,避免任何可能阻塞线程的东西,比如同步网络调用或无限循环。
一般来说,在大多数浏览器中,每个浏览器标签都有一个事件循环,以使每个进程都被隔离,避免一个有无限循环的网页或繁重的处理过程阻塞你整个浏览器。
环境管理多个并发的事件循环,以处理API调用为例。Web工作者也在他们自己的事件循环中运行。
你主要需要关注的是,你的代码会在一个事件循环中运行,写代码时要考虑到这件事,以避免阻塞它。
阻断事件循环
任何JavaScript代码如果花费太长的时间将控制权返回给事件循环,就会阻塞页面中任何JavaScript代码的执行,甚至阻塞UI线程,用户就无法点击周围,滚动页面,等等。
JavaScript中几乎所有的I/O原语都是无阻塞的。网络请求、文件系统操作,等等。被阻塞是一个例外,这就是为什么JavaScript基于回调,最近又基于承诺和async/await。
调用堆栈
调用堆栈是一个LIFO队列(Last In, First Out)。
事件循环不断地检查调用栈,看是否有任何需要运行的函数。
在这样做的时候,它将发现的任何函数调用添加到调用栈中,并按顺序执行每一个函数。
你知道你可能熟悉的错误堆栈跟踪,在调试器或浏览器控制台中?浏览器查找调用堆栈中的函数名称,以告知你当前调用是由哪个函数发起的。

一个简单的事件循环解释
让我们选一个例子。
我使用
foo,bar和baz作为随机名称。输入任何种类的名字来代替它们。
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
这段代码的打印结果
正如预期的那样。
当这段代码运行时,首先调用foo() 。在foo() ,我们首先调用bar() ,然后调用baz() 。
在这一点上,调用栈看起来像这样。

事件循环在每次迭代时都会查看调用栈中是否有东西,并执行它。

直到调用栈为空。
排队执行函数
上面的例子看起来很正常,没有什么特别的地方。JavaScript找到要执行的东西,按顺序运行它们。
我们来看看如何推迟一个函数,直到堆栈清空。
setTimeout(() => {}), 0) 的用例是调用一个函数,但要在代码中的其他函数都执行完后再执行它。
以此为例。
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
这段代码的打印,也许是令人惊讶的。
当这段代码运行时,首先调用foo()。在foo()里面,我们首先调用setTimeout,传递bar 作为参数,我们指示它立即以最快的速度运行,传递0作为定时器。然后我们调用 baz()。
在这一点上,调用栈看起来像这样。

这里是我们程序中所有函数的执行顺序。

为什么会出现这种情况?
消息队列
当setTimeout()被调用时,浏览器或Node.js会启动定时器。一旦计时器过期,在这种情况下,由于我们把0作为超时时间,回调函数就会被放到消息队列中。
消息队列也是用户发起的事件的地方,如点击或键盘事件,或获取响应,在你的代码有机会对其作出反应之前被排队。或者还有DOM事件,如onLoad 。
循环给了调用栈优先权,它首先处理它在调用栈中发现的所有东西,一旦里面没有东西了,它就去捡起消息队列中的东西。
我们不必等待像setTimeout, fetch或其他东西的函数来做自己的工作,因为它们是由浏览器提供的,而且它们生活在自己的线程中。例如,如果你把setTimeout 的超时设置为2秒,你就不必等待2秒--等待发生在其他地方。
ES6作业队列
ECMAScript 2015引入了Job Queue的概念,它被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
这是承诺(以及建立在承诺之上的Async/await)和通过setTimeout() 或其他平台API的普通异步函数之间的巨大区别。
总结
这篇文章向你介绍了Node.js事件循环的基本构建模块。
这是用Node.js编写的任何程序的一个重要部分,我希望这里解释的一些概念对你将来会有帮助。