我已经将这个问题的答案写在另一个答案中作为旁白。通常我会关闭这个问题作为重复并指向该答案,但这是一个非常不同的问题。另一个问题是关于 javascript 性能的。为了回答这个问题,我必须先写下这个问题的答案。
因此,我将做一些通常不应该做的事情:我将把我的部分答案复制到另一个问题上。所以这是我的答案:
javascript 和 node.js 等待的实际事件根本不需要循环。事实上,它们需要 0% 的 CPU 时间。
异步 I/O 如何工作(在任何编程语言中)
硬件
如果我们真的需要了解节点(或浏览器)内部是如何工作的,不幸的是,我们必须首先了解计算机是如何工作的——从硬件到操作系统。是的,这将是一次深潜,所以请耐心等待。
这一切都始于中断的发明。
这是一项伟大的发明,也是一盒潘多拉 - Edsger Dijkstra
是的,上面的引用来自同一个“Goto 被认为有害”Dijkstra。从一开始,将异步操作引入计算机硬件就被认为是一个非常困难的话题,即使对于行业中的一些传奇人物也是如此。
引入中断是为了加速 I/O 操作。硬件不需要在无限循环中使用软件轮询某些输入(从有用的工作中占用 CPU 时间),硬件将向 CPU 发送信号以告诉它发生了事件。然后 CPU 将挂起当前正在运行的程序并执行另一个程序来处理中断——因此我们称这些函数为中断处理程序。“处理程序”这个词一直在堆栈中一直停留在调用回调函数“事件处理程序”的 GUI 库中。
如果您不熟悉并想了解更多信息,维基百科实际上有一篇关于中断的相当不错的文章:https : //en.wikipedia.org/wiki/Interrupt。
如果您一直在注意,您会注意到中断处理程序的这个概念实际上是一个回调。将 CPU 配置为在稍后发生事件时调用函数。所以即使是回调也不是一个新概念——它比 C 古老得多。
操作系统
中断使现代操作系统成为可能。如果没有中断,CPU 将无法暂时停止您的程序运行操作系统(好吧,有协作多任务处理,但现在让我们忽略它)。操作系统的工作原理是它在 CPU 中设置一个硬件定时器来触发中断,然后它告诉 CPU 执行您的程序。正是这种周期性的定时器中断运行您的操作系统。
除了定时器,操作系统(或者更确切地说是设备驱动程序)为 I/O 设置中断。当 I/O 事件发生时,操作系统将接管您的 CPU(或多核系统中的 CPU 之一)并检查其数据结构,它接下来需要执行哪个进程来处理 I/O(这称为抢占式多任务处理)。
从键盘和鼠标到存储到网卡,一切都使用中断来告诉系统有数据要读取。如果没有这些中断,监控所有这些输入将占用大量 CPU 资源。中断非常重要,以至于它们经常被设计成 I/O 标准,如 USB 和 PCI。
流程
现在我们已经清楚地了解了这一点,我们可以理解 node/javascript 如何实际处理 I/O 和事件。
对于 I/O,各种操作系统都有各种不同的 API 来提供异步 I/O——从 Windows 上的重叠 I/O 到 Linux 上的 poll/epoll,再到 BSD 上的 kqueue 到跨平台 select()。Node 在内部使用 libuv 作为对这些 API 的高级抽象。
尽管细节不同,但这些 API 的工作方式相似。本质上,它们提供了一个函数,当调用该函数时将阻塞您的线程,直到操作系统向其发送事件。所以是的,即使是非阻塞 I/O 也会阻塞你的线程。这里的关键是阻塞 I/O 会在多个地方阻塞你的线程,但非阻塞 I/O 只会在一个地方阻塞你的线程——在那里你等待事件。
查看我对另一个问题的回答,以获取有关此类 API 如何在 C/C++ 级别工作的更具体示例:我知道回调函数异步运行,但为什么呢?
对于按钮单击和鼠标移动等 GUI 事件,操作系统只需跟踪鼠标和键盘中断,然后将它们转换为 UI 事件。这使您的软件表单无需知道按钮、窗口、图标等的位置。
这允许您以面向事件的方式设计您的程序。这类似于中断允许 OS 设计人员实现多任务处理的方式。实际上,异步 I/O 之于框架就像中断之于操作系统一样。它允许 javascript 花费恰好 0% 的 CPU 时间来处理(等待)I/O。这就是使异步代码快速的原因——它并不是真的更快,但不会浪费时间等待。
这个答案很长,所以我会留下我对与此主题相关的其他问题的答案的链接:
Node js 架构和性能(注意:这个答案提供了一些关于事件和线程关系的见解 - tldr:操作系统在内核事件之上实现线程)
javascript 是否使用弹性赛道算法进行处理
node.js 服务器如何优于基于线程的服务器