JavaScript中的事件循环与Node.js中的异步非阻塞I/O有什么区别?

17

在这个关于“Node.js中的非阻塞或异步I/O是什么”的answer中,描述听起来与vanilla js中的事件循环没有区别。它们之间有区别吗?如果没有,那么事件循环是否只是被重新命名为“异步非阻塞I/O”,以便更容易地将Node.js销售给其他选项?


2
是的!“绿色线程”是一个相对较新的概念,通过NodeJS引入到服务器中(在Java、C#等语言中并不常见)。它将JavaScript中的事件循环概念带到了服务器运行时。 - Jonas Wilms
嗯...我不认为事件循环只是被重新命名为“异步非阻塞I/O”。在两种情况下,事件循环的目的是相同的,但回调队列是Web API(Vanilla js)和C/C++ API即libuv库-重点关注Node.js的异步I/O。因此,我认为libuv库在这里是关键...我从Philip Roberts的视频中理解了这个概念-https://www.youtube.com/watch?v=8aGhZQkoFbQ。 - iravinandan
3个回答

11

事件循环是机制。异步 I/O 是目标。

异步 I/O 是一种编程风格,其中 I/O 调用在返回之前不会等待操作完成,而只是安排调用者在发生这种情况时得到通知,并将结果返回到某个地方。在 JavaScript 中,通常通过调用回调函数或解析 Promise 来执行通知。对于程序员来说,这是无关紧要的: 我请求操作,当它完成后,我会得到通知。

事件循环通常是实现这一目标的方式。问题在于,在大多数 JavaScript 实现中,实际上有一个循环,最终简化为:

while (poll_event(&ev)) {
    dispatch_event(&ev);
}

通过安排操作完成事件在循环中作为事件接收,并将其分派到调用者选择的回调函数来执行异步操作。

有一些实现异步编程的方式并不基于事件循环,例如使用线程和条件变量。但是由于历史原因,这种编程风格在JavaScript中相当难以实现。因此,在实践中,JavaScript中异步性的主要实现是基于从全局事件循环中分派回调。

换句话说,“事件循环”描述了宿主所做的事情,而“异步I/O”描述了程序员所做的事情。

从一个非程序员的俯视角度来看,这似乎是纠缠不清,但这种区别有时可能很重要。


8
有两种不同的事件循环:
  1. 浏览器事件循环
  2. NodeJS 事件循环

浏览器事件循环

事件循环是一个持续运行的进程,执行任何排队的任务。它有多个任务来源,可以保证在该来源内有执行顺序,但浏览器可以选择每次循环从哪个来源获取任务。这使得浏览器可以优先考虑性能敏感任务,如用户输入。

浏览器事件循环会不断检查以下几个步骤:

  • 任务队列 - 可以有多个任务队列。浏览器可以按任意顺序执行队列。同一队列中的任务必须按它们到达的顺序执行,先进先出。任务按顺序执行,并且浏览器可以在任务之间渲染。来自相同来源的任务必须放入同一队列。重要的是任务将从头到尾运行。每个任务后,事件循环都会转到微任务队列并完成其中所有任务。

  • 微任务队列 - 微任务队列在每个任务结束时处理。在微任务期间排队的任何其他微任务都将添加到队列的末尾并进行处理。

  • 动画回调队列 - 动画回调队列在像素重绘之前处理。队列中的所有动画任务都将被处理,但在动画任务期间排队的任何其他动画任务都将计划在下一帧执行。

  • 渲染管道 - 在此步骤中,将进行渲染。浏览器可以决定何时执行此操作,并尝试尽可能高效地进行。仅当有值得更新的内容时才会发生渲染步骤。大多数屏幕以固定频率更新,在大多数情况下为 60 次/秒 (60Hz)。因此,如果我们每秒更改页面样式 1000 次,则渲染步骤将不会被处理 1000 次/秒,而是将自行同步到显示器并仅呈现可用于显示的频率。

需要注意的重要事项是 Web API,它们实际上是线程。例如,setTimeout() 是由浏览器提供给我们的 API。调用 setTimeout() 后,Web API 将接管并处理它,并将结果作为新任务放入任务队列中的主线程返回。

我找到的最好的描述事件循环的视频是这个。当我调查事件循环的工作方式时,它对我很有帮助。另外两个不错的视频是这个这个。你应该看看所有这些视频。

NodeJS事件循环

NodeJS事件循环通过尽可能地将操作卸载到系统内核来实现非阻塞操作。大多数现代内核都是多线程的,它们可以在后台执行多个操作。当其中一个操作完成时,内核会告诉NodeJS。

提供事件循环给NodeJS的库称为Libuv。它默认会创建一个名为“Thread Pool”的线程池,用于卸载异步工作。如果需要,你还可以更改线程池中线程的数量。

NodeJS事件循环经过不同的阶段:

  • 计时器阶段(timers) - 此阶段执行由setTimeout()setInterval()调度的回调。

  • 待定回调阶段(pending callbacks) - 执行延迟到下一个循环迭代的I/O回调。

  • 空闲准备阶段(idle, prepare) - 仅在内部使用。

  • 轮询阶段(poll) - 检索新的I/O事件;执行与I/O相关联的回调(除了定时器、setImmediate()调度的回调和关闭回调之外的所有回调)。适当时,Node将在此处阻塞。

  • 检查阶段(check) - 在此执行setImmediate()调用的回调。

  • 关闭回调阶段(close callbacks) - 一些关闭回调,例如 socket.on('close', ...)

每次事件循环运行之间,Node.js会检查是否正在等待任何异步I/O或定时器,如果没有,则会干净地关闭。

在浏览器中,我们有Web API。 在NodeJS中,我们有C++ API,并遵循相同的规则。

如果你想要更多信息,可以参考这个视频


1
  • 多年以来,JavaScript 只能用于客户端应用程序,如运行在浏览器上的交互式 Web 应用程序。使用 NodeJS,JavaScript 可以用于开发服务器端应用程序。尽管这是同一种编程语言,但客户端和服务器端有不同的要求。

  • 事件循环”是一种通用的编程模式,JavaScript/NodeJS 的事件循环也不例外。事件循环不断地监视任何排队的事件处理程序,并相应地处理它们。

  • 在浏览器的上下文中,“事件”是指网页上的用户交互(例如点击、鼠标移动、键盘事件等)。但在 Node 的上下文中,事件是异步的服务器端操作(例如文件 I/O 访问、网络 I/O 等)。


由于当前回答写得不清楚,请[编辑]以添加更多细节,帮助他人理解如何解决所问的问题。您可以在帮助中心找到有关编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接