How Node.js Works Behind the Scenes
Node.js 在幕后的工作原理
After you complete this article, you will have a solid understanding of:
完成本文后,您将对以下内容有深入的了解:
- How the Node.js Event Loop works, including all of its phases
Node.js Event Loop 的工作原理,包括其所有阶段 - How Node.js executes JavaScript code in a single-threaded environment
Node.js 如何在单线程环境中执行 JavaScript 代码 - How Node.js uses the libuv library and internal APIs to manage asynchronous operations
Node.js 如何使用 libuv 库和内部 API 管理异步作 - Why understanding the Event Loop is crucial for writing efficient backend code
为什么理解 Event Loop 对于编写高效的后端代码至关重要
If you know how JavaScript works behind the scenes in the browser environment (I highly recommend you check this 15-minute read article if you don't know), it's relatively easier to understand unlike Node.js. It doesn't have a bunch of phases or so on.
如果你知道 JavaScript 在浏览器环境的幕后是如何工作的(如果你不知道,我强烈建议你看看这篇 15 分钟的阅读文章),它与 Node.js 不同,它相对更容易理解。它没有一堆阶段之类的。
But why does Node.js have different, and complicated phases, just to make things harder to understand?
但是,为什么 Node.js 有不同的、复杂的阶段,只是为了让事情更难理解呢?
In the browser, JavaScript only needs to handle basic things like user interactions, timers, and promises. So it only uses a task queue and a microtask queue, which is enough for that environment. It handles these kinds of operations with the help of Web APIs.
在浏览器中,JavaScript 只需要处理用户交互、计时器和 promise 等基本内容。所以它只使用一个任务队列和一个微任务队列,这对于那个环境来说已经足够了。它在 Web API 的帮助下处理这些类型的作。
Note: If you are not familiar with task queue and microtask queue, as I said before - and promise will say for the last time - you can check this 15-minute explanation about how JavaScript works
注意:如果你不熟悉任务队列和微任务队列,就像我之前说的 - promise 将最后一次说 - 你可以查看这个关于 JavaScript 工作原理的 15 分钟解释
But Node.js is not just JavaScript - it's a full runtime designed to run JavaScript anywhere! There is nothing like Web APIs that can help it. That runtime has to work on our servers so it also deals with files, network requests, and low-level system tasks. Because of this, Node.js needs a more complex event loop with multiple phases to handle all these different types of operations properly.
但 Node.js 不仅仅是 JavaScript - 它是一个完整的运行时,旨在在任何地方运行 JavaScript!没有什么比 Web API 更能帮助它了。该运行时必须在我们的服务器上工作,因此它还处理文件、网络请求和低级系统任务。因此,Node.js 需要一个更复杂的事件循环,其中包含多个阶段,以正确处理所有这些不同类型的作。
Node.js has to make JavaScript work everywhere with good performance. So it has to handle what browsers handle for us plus some extra server-side events that are very important to be handled properly!
Node.js 必须使 JavaScript 在任何地方都能正常工作,并具有良好的性能。因此,它必须处理浏览器为我们处理的内容,以及一些额外的服务器端事件,这些事件对于正确处理非常重要!
While in web browsers, things like fetch requests are sent to the Web API, in Node.js this works a bit differently. In Node.js, the equivalent of the "Web API" concept is actually a C/C++ library called "libuv" - they're not perfectly equal but think like they both try to make JavaScript asynchronous.
在 Web 浏览器中,获取请求等内容会发送到 Web API,而在 Node.js 中,其工作方式略有不同。在 Node.js 中,“Web API”概念的等价物实际上是一个名为“libuv”的 C/C++ 库 - 它们并不完全相等,但认为它们都试图使 JavaScript 异步。
Note: I will be using the term "I/O" a lot in this article. I/O means Input-Output, so it refers to any operation where data goes in or out to/from outside of our world - like reading a file, sending a request, or receiving a response.
注意:在本文中,我将大量使用术语 “I/O”。I/O 表示 Input-Output,因此它指的是数据进出我们世界之外的任何作 - 例如读取文件、发送请求或接收响应。
Web API in web browsers:
Web 浏览器中的 Web API:
- It's an interface provided by the browser itself.
它是浏览器本身提供的界面。 - Implemented by browser manufacturers (Chrome, Firefox, Safari, etc.) in languages like C++.
由浏览器制造商(Chrome、Firefox、Safari 等)以 C++ 等语言实现。 - Each browser has its own Web API implementation.
每个浏览器都有自己的 Web API 实现。 - Works within the browser, using the browser's resources.
在浏览器中工作,使用浏览器的资源。
libuv in Node.js: Node.js 中的 libuv:
- It's a C library used by Node.js to manage asynchronous I/O operations.
它是 Node.js 用来管理异步 I/O 作的 C 库。 - Directly uses the operating system's APIs.
直接使用作系统的 API。 - Abstracts operating system-specific mechanisms (IOCP on Windows, epoll on Linux, etc.).
抽象了特定于作系统的机制(Windows 上的 IOCP、Linux 上的 epoll 等)。 - An independent component that comes with Node.js.
Node.js 附带的独立组件。
Key difference: While Web API works within the browser, libuv directly accesses operating system resources. Both manage asynchronous operations but work in different environments.
主要区别: Web API 在浏览器中工作,而 libuv 直接访问作系统资源。两者都管理异步作,但在不同的环境中工作。
In terms of request handling:
在请求处理方面:
- In Web API: Requests wait in the browser's own subsystems.
在 Web API 中:请求在浏览器自己的子系统中等待。 - In Node.js: Requests wait in the operating system's I/O queues.
Node.js:请求在作系统的 I/O 队列中等待。
In both cases, JavaScript's main thread isn't blocked, but requests are managed at different layers. While the web browser manages the process within its own resources and processes, Node.js works more directly with the mechanisms provided by the operating system.
在这两种情况下,JavaScript 的主线程都不会被阻止,但请求会在不同的层进行管理。虽然 Web 浏览器在其自己的资源和进程中管理进程,但 Node.js 更直接地使用作系统提供的机制。
And again in both cases, the operating system is the main mechanism that monitors and notifies when I/O operations are completed. The operating system detects network responses, file operations, and other I/O events using hardware interrupts and system calls.
同样,在这两种情况下,作系统都是监视 I/O 作完成并发出通知的主要机制。作系统使用硬件中断和系统调用来检测网络响应、文件作和其他 I/O 事件。
Now, you have some idea about how Node.js actually handles things differently from JavaScript. If you didn't completely get it, that's all fine! We will go step by step from the very basic concepts, to understand everything clearly.
现在,你对 Node.js 实际处理事情的方式与 JavaScript 有了一些不同的看法。如果你没有完全理解它,那也没关系!我们将从最基本的概念一步一步地进行,以清楚地理解所有内容。
Node Execution 节点执行
The Node runtime is just a C++ program that takes an input, and the input is a JavaScript file. Node reads that file and interprets that JavaScript. JS Code is executed line by line serially until it's done.
Node 运行时只是一个接受输入的 C++ 程序,而输入是一个 JavaScript 文件。Node 读取该文件并解释该 JavaScript。JS 代码是逐行串行执行的,直到完成。
But the real world isn't all sunshine and rainbows. Our JavaScript code is not just read line by line and finishes. There are some situations that may, and will happen. For example, we can say execute this part of code after X milliseconds. So until those milliseconds have passed, we need to wait. Or we may read a file from the external world, and after reading a file is done, only then we might want to execute some code. Or let's say our JavaScript code is listening on a port, and someone is connected to us. So we can't just terminate the program, right? We need to wait. When a user is connected to us, we don't even know when they're going to send a request. But when they do, we immediately need to take action, right? So serial execution - executed line by line serially - does not work anymore.
但现实世界并不全是阳光和彩虹。我们的 JavaScript 代码不仅仅是逐行读取并完成。有些情况可能会发生,而且将会发生。例如,我们可以说在 X 毫秒后执行这部分代码。因此,在这些毫秒过去之前,我们需要等待。或者我们可以从外部世界读取一个文件,读取文件完成后,我们可能才想要执行一些代码。或者假设我们的 JavaScript 代码正在侦听一个端口,并且有人连接到我们。所以我们不能就这样终止这个项目,对吧?我们需要等待。当用户连接到我们时,我们甚至不知道他们何时会发送请求。但是当他们这样做时,我们需要立即采取行动,对吧?所以串行执行 - 逐行串行执行 - 不再有效。
So we have something like a loop to check if some things are happening or not. Because when they happen, our code needs to take action immediately.
所以我们有一个类似循环的东西来检查某些事情是否正在发生。因为当它们发生时,我们的代码需要立即采取行动。
That's why we have something called an event loop.
这就是为什么我们有一个叫做事件循环的东西。
Note: Before we dive into the event loop, there is another term that you need to know, and it's callback. Callback functions are functions that need to execute when a specific event happens. So when we specify a timer, we also specify a callback which will execute after the timer has finished. Or we say go read this file, and after reading, you need to execute this function. As the name says "Call back" - calling back a function after some event happens.
注意:在我们深入研究事件循环之前,你需要了解另一个术语,那就是 callback。回调函数是在特定事件发生时需要执行的函数。因此,当我们指定计时器时,我们还指定了一个回调,该回调将在计时器完成后执行。或者我们说 go read 这个文件,读完之后,你需要执行这个函数。顾名思义,“Call back” - 在某些事件发生后回调函数。
Event loop is a loop with different phases. Each phase has a queue of callbacks. And our code will terminate itself when there are no callbacks left in these queues. Don't worry about the details for right now. Because we will deep dive into the main loop and its every phase. You will have a clear understanding.
事件循环是一个具有不同阶段的循环。每个阶段都有一个回调队列。当这些队列中没有回调时,我们的代码将自行终止。现在不用担心细节。因为我们将深入研究 main loop 及其每个阶段。你会有一个清晰的理解。
Event Loop (Simplified) 事件循环(简化)
The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that a single JavaScript thread is used by default — by offloading operations to the system kernel whenever possible.
事件循环允许 Node.js 执行非阻塞 I/O 作(尽管默认情况下使用单个 JavaScript 线程)尽可能将作卸载到系统内核。
So when initial code execution happens, we start reading the code line by line. If we see any async operation on our way (Reading a file, Network Connection, Timers...) we register the callback. We don't execute its callback! We just register it so that when our event happens at a later time, we can execute it. Callbacks may register more callbacks too. Maybe we said go read a file after 5 seconds, and boom our callback registered another callback. We have to keep running our code after these callback queues are empty.
因此,当初始代码执行发生时,我们开始逐行读取代码。如果我们在路上看到任何异步作(读取文件、网络连接、计时器等),我们就会注册回调。我们不执行它的回调!我们只需注册它,以便当我们的事件在稍后发生时,我们可以执行它。回调也可以注册更多回调。也许我们说 5 秒后去读取一个文件,然后 boom 我们的回调注册了另一个回调。在这些回调队列为空后,我们必须继续运行我们的代码。
So what we do is, execution -> go to callback queue -> execution -> go to callback queue... until there are no callbacks to call.
所以我们要做的是,执行 -> 去回调队列 -> 执行 -> 去回调队列...直到没有可调用的回调。
But remember, this loop starts after execution of initial code! Execution of the initial code can also be considered a phase.
但请记住,此循环在执行初始代码后开始!初始代码的执行也可以被视为一个阶段。
Now, let's go over all these phases of our code execution. We will learn what's happening in our code step by step.
现在,让我们回顾一下代码执行的所有这些阶段。我们将逐步了解代码中发生的情况。
The Main Module 主模块
First, we start with The Main Module. This is where we don't have a loop yet, this is the initial phase - if you want to call it a phase, you may not want to either, I won't blame you.
首先,我们从 Main Module 开始。这是我们还没有循环的地方,这是初始阶段 - 如果你想称它为阶段,你可能也不想,我不会责怪你。
The initial phase will run only once at the begging of our code execution!
初始阶段将仅在我们的代码执行请求下运行一次!
In the main module, all code executes synchronously. No callbacks can get executed, and the main loop is not yet initialized. We will register callbacks, but we won't execute them in this phase. Main module = initial phase.
在主模块中,所有代码都同步执行。无法执行任何回调,并且 main 循环尚未初始化。我们将注册回调,但在此阶段不会执行它们。主模块 = 初始阶段。
const x = 1;
const y = x + 1;
setTimeout(() => console.log("Should run in 1ms"), 1);
for (let i = 0; i < 10000000; i++);
console.log("When this will be printed?");
So, what do you think the output will be? If you read what I just wrote carefully, you probably guessed it right.
那么,您认为输出会是什么?如果你仔细阅读我刚才写的内容,你可能猜对了。
When this will be printed?
Should run in 1ms
It's the main module and there are no callbacks executed in this phase! It's the initial phase, and the initial execution of our code.
它是主模块,此阶段没有执行任何回调!这是我们代码的初始阶段和初始执行。
Most of the time (if you use require) other modules will be resolved before the initial phase even starts. If other modules have other modules inside them, then they have to wait for those modules to be loaded. And we have to wait for all of the modules to be loaded.
大多数时候(如果你使用 require)其他模块将在初始阶段开始之前被解决。如果其他模块内部有其他模块,那么它们必须等待这些模块被加载。我们必须等待所有模块都加载完毕。
We need to keep the initial phase as short as possible. Keeping the initial phase short is great for performance, since it spins up Node faster. So using too many modules in our code is almost never a good thing.
我们需要使初始阶段尽可能简短。保持较短的初始阶段对性能非常有益,因为它可以更快地启动 Node。因此,在我们的代码中使用太多模块几乎从来都不是一件好事。
Well, technically since the loop hasn't even started in the initial phase - main module - you can prefer not to say it was a "phase". But now, the loop is going to start! And the loop's first phase will be the timer phase. With this phase starting, we will have the event loop that we've talked about at the beginning. Now, we will see every step, and these will be the steps that we will go through:
嗯,从技术上讲,由于循环甚至还没有在初始阶段 - 主模块 - 你宁愿不说它是一个 “阶段” 。但是现在,循环要开始了!循环的第一阶段将是 timer 阶段。随着这个阶段的开始,我们将有我们在开始时讨论过的事件循环。现在,我们将看到每一步,这些将是我们将要经历的步骤:
Each box will be referred to as a "phase" of the event loop.
每个框将称为事件循环的 “阶段”。
Each phase has a FIFO (First in, first out) queue of callbacks to execute. While each phase is special in its own way, generally, when the event loop enters a given phase, it will perform any operations specific to that phase, then execute callbacks in that phase's queue until the queue has been exhausted or the maximum number of callbacks has executed. When the queue has been exhausted or the callback limit is reached, the event loop will move to the next phase, and so on.
每个阶段都有一个要执行的 FIFO (先进先出) 回调队列。虽然每个 phase 都有自己的特殊之处,但通常,当 event loop 进入给定的 phase 时,它会执行特定于该 phase 的任何作,然后在该 phase 的队列中执行回调,直到队列耗尽或执行了最大数量的回调。当队列耗尽或达到回调限制时,事件循环将进入下一阶段,依此类推。
This multi-phase approach allows Node.js to prioritize different types of asynchronous operations efficiently, which is very important for server-side applications that handle many I/O operations like file system access, network requests, and database queries.
The browser's simpler model works well for user interface interactions, while Node.js's more complex model and phases are optimized for server-side operations where different types of I/O tasks need different handling priorities.
这种多阶段方法使 Node.js 能够有效地确定不同类型的异步作的优先级,这对于处理许多 I/O 作(如文件系统访问、网络请求和数据库查询)的服务器端应用程序非常重要。浏览器的 Simpler 模型适用于用户界面交互,而 Node.js 更复杂的模型和阶段针对服务器端作进行了优化,其中不同类型的 I/O 任务需要不同的处理优先级。
Before we deep dive into every step, let's look at an overview so you may have an idea before you start understanding the detail of every step.
在我们深入研究每个步骤之前,让我们看一下概述,以便您在开始了解每个步骤的细节之前可能会有所了解。
Phases Overview 阶段概述
In Node.js, the event loop continuously iterates through all six phases in sequence. This constant circulation ensures that all types of asynchronous operations are properly handled, with the event loop checking each phase for pending tasks before moving to the next one, and then starting over again from the beginning once it completes a full cycle.
在 Node.js 中,事件循环按顺序持续迭代所有 6 个阶段。这种持续循环确保了所有类型的异步作都得到正确处理,事件循环在进入下一个阶段之前检查每个阶段是否有待处理任务,然后在完成一个完整周期后从头开始。
How these 6 phases are placed in their own positions is very important. They are not just randomly placed 6 phases, they all have specific phases for a lot of reasons. And when we go in detail, you will learn some of the reasons why some loops are placed after/before other phases.
如何将这 6 个阶段放置在各自的位置非常重要。它们不仅仅是随机放置的 6 个阶段,由于很多原因,它们都有特定的阶段。当我们详细介绍时,您将了解一些 Loop 被放置在其他阶段之后/之前的一些原因。
-
Timers: This phase executes callbacks scheduled by
setTimeout()
andsetInterval()
.
计时器:此阶段执行由setTimeout()
和setInterval()
安排的回调。 -
Pending Callbacks: This is where Node.js deals with "leftover" callbacks from the previous loop, especially those related to system operations. These are typically callbacks related to system operations like TCP error handling.
Pending Callbacks:这是 Node.js 处理上一个循环的 “剩余” 回调的地方,尤其是与系统作相关的回调。这些通常是与系统作(如 TCP 错误处理)相关的回调。 -
idle, prepare: These are internal phases that Node.js uses for its own housekeeping. The idle phase is where Node.js might perform some internal cleanup tasks when the loop has nothing else to do. The prepare phase is where Node.js gets ready to enter the poll phase, setting up anything needed before checking for new I/O events.
idle, prepare:这些是 Node.js 用于自己内务管理的内部阶段。空闲阶段是当循环无事可做时 Node.js 可能会执行一些内部清理任务的地方。准备阶段是 Node.js 准备进入轮询阶段的地方,在检查新的 I/O 事件之前设置任何需要的内容。 -
Poll: This is arguably the most important phase. Here, Node.js:
Poll:这可以说是最重要的阶段。在这里,Node.js:- Looks for new I/O events (like incoming network connections or file operations)
查找新的 I/O 事件(如传入的网络连接或文件作) - Executes callbacks for those I/O events that are ready
为那些准备好的 I/O 事件执行回调 - May temporarily pause here ("block") to wait for new events if there's nothing else to do.
如果没有其他事情可做,可以在此处暂时暂停 (“block”) 以等待新事件。
The poll phase is essentially where Node.js spends most of its time, waiting for and responding to external events like network requests or file operations.
轮询阶段本质上是 Node.js 花费大部分时间等待和响应外部事件(如网络请求或文件作)的地方。 - Looks for new I/O events (like incoming network connections or file operations)
-
Check: This is specifically where
setImmediate()
callbacks run.setImmediate()
is a special timing function in Node.js that lets you schedule a callback to run immediately after the poll phase. It's useful when you want to execute code right after any pending I/O operations but before any new timers.
检查:这是setImmediate()
回调运行的具体位置。setImmediate()
是 Node.js 中的一个特殊计时函数,它允许你安排一个回调在 poll 阶段后立即运行。当您想要在任何待处理的 I/O 作之后但在任何新计时器之前执行代码时,它非常有用。 -
Close Callbacks: This final phase handles cleanup callbacks - specifically those related to closing resources like sockets or file handles. For example, when a network connection closes, the socket's 'close' event callback would run in this phase.
Close Callbacks:最后阶段处理清理回调 - 特别是与关闭 sockets 或 file handles 等资源相关的回调。例如,当网络连接关闭时,套接字的 'close' 事件回调将在此阶段运行。
It's okay if you didn't understand at all what actually happens in every phase. Because we will learn all of them in detail starting right now! After you completed this blog, I want you to come back in this part, and read it again. You will see how much you actually learned.
如果你完全不了解每个阶段实际发生的事情,那也没关系。因为从现在开始,我们将详细学习所有这些!在您完成此博客后,我希望您返回这一部分,并再次阅读它。你会看到你实际学到了多少。
Phases in Detail 阶段详情
Timers Phase 计时器阶段
Timers phase starts right after the initial phase. This is where the event main loop actually gets initialized. And the first phase in the loop is timers. It's done by the underlying library called libuv.
计时器阶段在初始阶段之后立即开始。这是事件 main 循环实际初始化的地方。循环的第一阶段是 timers。它由名为 libuv 的底层库完成。
Timer callbacks get scheduled, sorted by duration.
Timer 回调被安排,按 duration 排序。
But they are not 100% accurate! They can be slowed down by our operating system, and also by other phases too. For example, if we're doing a heavy operation in the initial phase, that means our timer has to wait at least until that heavy operation ends. So when we say, run this function after 10 milliseconds, it can run it maybe even after 2 seconds. That's why we say they are not accurate.
但它们并不是 100% 准确的!我们的作系统可能会减慢它们的速度,也可以被其他阶段减慢。例如,如果我们在初始阶段执行 heavy 作,这意味着我们的 timer 必须至少等到该 heavy 作结束。因此,当我们说,在 10 毫秒后运行这个函数时,它甚至可以在 2 秒后运行它。这就是为什么我们说它们不准确。
Now, let's see a code example to understand everything better. We are going to set some timers, and see if they actually work when the "correct" time passes.
现在,让我们看一个代码示例来更好地理解所有内容。我们将设置一些计时器,看看当 “正确” 时间过去时它们是否真的工作。
First, let's see how setTimeout function works.
首先,让我们看看 setTimeout 函数是如何工作的。
setTimeout(timerCallback, 100, "100 ms", 100);
When we define a setTimeout function in Node.js, the first argument will be the function that will be executed after the second argument milliseconds. The third and fourth arguments are arguments that are passed to the timerCallback
function. So:
当我们在 Node.js 中定义 setTimeout 函数时,第一个参数将是将在第二个参数毫秒后执行的函数。第三个和第四个参数是传递给 timerCallback
函数的参数。所以:
const timerCallback = (a, b) =>
console.log(`Timer ${a} delayed for ${Date.now() - start - b}`);
const start = Date.now();
setTimeout(timerCallback, 500, "500 ms", 500);
setTimeout(timerCallback, 0, "0 ms", 0);
setTimeout(timerCallback, 1, "1 ms", 1);
setTimeout(timerCallback, 1000, "1000 ms", 1000);
for (let i = 0; i <= 1000000000; i++);
Note: arguments "a", and "b" will take the third and fourth arguments in the setTimeout function.
注意:参数 “a” 和 “b” 将采用 setTimeout 函数中的第三个和第四个参数。
In this example, we are trying to find how many milliseconds they have been delayed.
First, we take the time by defining a start
variable to find the exact time when we started the initial phase. And inside the timerCallback function, we take the Date.now(). This Date.now() will give us the exact time that the function has executed, right? So if we say Date.now() - start time when the function is executed, we should get 500ms for the first function, 0ms for the second function, 1ms for the third function, 1000ms for the fourth function. Because they should work after "second argument" ms.
So function execution's time
- start time
= the time that has passed until they executed
.
In this example, b gives us how many ms they will be delayed. So, Date.now() - start - b
should be equal to 0.
在此示例中,我们尝试查找它们延迟了多少毫秒。首先,我们花时间定义一个 start
变量来查找我们开始初始阶段的确切时间。在 timerCallback 函数中,我们采用 Date.now()。这个 Date.now() 会给我们函数执行的确切时间,对吧?因此,如果我们说 Date.now() - 函数执行时的开始时间,我们应该为第一个函数获得 500 毫秒,第二个函数为 0 毫秒,第三个函数为 1 毫秒,第四个函数为 1000 毫秒。因为它们应该在 “second argument” ms 之后工作。所以函数 execution's time
- start time
= the time that has passed until they executed
.在这个例子中,b 给出了它们将被延迟多少毫秒。所以, Date.now() - start - b
应该等于 0。
Well, that's not really true though. Because when you work with timers, you will always be delayed by something. By your operating system, by other phases... Remember, before we go into the loop, we have the initial phase. In the initial phase, these functions are registered but not called! So first we have to be done with the initial phase. And that's why at least we will be delayed because of the initial phase. That's because there is some "heavy" operation going on in the initial phase. (for loop to demonstrate)
嗯,这并不是真的。因为当您使用计时器时,您总是会被某些事情延迟。通过您的作系统,通过其他阶段...请记住,在我们进入循环之前,我们有一个初始阶段。在初始阶段,这些函数被注册但未被调用!所以首先我们必须完成初始阶段。这就是为什么至少我们会因为初始阶段而被推迟。那是因为在初始阶段有一些 “繁重 ”的作正在进行。(for 循环演示)
So our output will be:
所以我们的输出将是:
Timer 0 ms delayed for 437
Timer 1 ms delayed for 442
Timer 500 ms delayed for 2
Timer 1000 ms delayed for 2
We can say, our heavy operation in the initial phase took ~437ms. Because the timer that should run immediately (after 0ms), ran after 437ms. That's why our second (1ms timer) has delayed too. (Since Node.js is single threaded, they have to wait for each other!) But it didn't delay 438ms as expected. It's delayed 442ms so it's even 4ms more. This proves what we've said, timers are not 100% accurate.
可以说,我们在初始阶段的繁重作花费了 ~437 毫秒。因为应该立即运行的计时器 (0ms 后) 在 437ms 后运行。这就是为什么我们的第二个 (1ms timer) 也延迟了。(由于 Node.js 是单线程的,因此它们必须相互等待!但它并没有像预期的那样延迟 438 毫秒。它延迟了 442 毫秒,所以甚至多了 4 毫秒。这证明了我们所说的,计时器并不是 100% 准确的。
Now, we go into the third phase. (second phase in the loop, third phase overall). And it's called Pending Callbacks.
现在,我们进入第三阶段。(循环中的第二阶段,整体第三阶段)。它被称为 Pending Callbacks。
Pending Callbacks 待处理回调
The pending callbacks phase is specifically responsible for executing callbacks that were delayed from previous loop iterations, typically those related to I/O operations (for example like TCP connection errors).
待处理回调阶段专门负责执行从先前循环迭代中延迟的回调,通常是与 I/O 作相关的回调(例如,TCP 连接错误)。
Here's what happens during this phase:
以下是此阶段发生的情况:
- When certain system operations (like TCP errors) occur in the poll phase - which is another phase that you will learn - their callbacks get scheduled for execution in this phase.
当某些系统作(如 TCP 错误)发生在轮询阶段时(这是您将学习的另一个阶段),它们的回调将被安排在此阶段执行。 - Node.js processes these pending callbacks one after another until either the queue is empty or the system-specific limit is reached.
Node.js 会一个接一个地处理这些待处理的回调,直到队列为空或达到系统特定的限制。
Think of it as a "cleanup" phase where Node.js handles callbacks that were deferred from previous operations.
将其视为一个 “清理” 阶段,在该阶段 Node.js 处理从先前作中推迟的回调。
Like we said, in each iteration, the pending callbacks phase processes callbacks that were deferred from previous iterations. So when a callback is deferred, it will be processed in the pending callbacks' phase of the next iteration.
正如我们所说,在每次迭代中,pending callbacks 阶段都会处理从先前迭代中推迟的回调。因此,当回调被延迟时,它将在下一次迭代的待处理回调阶段进行处理。
Let's say you're building a web server that needs to handle many connections:
假设您正在构建一个需要处理许多连接的 Web 服务器:
const http = require("http");
const fs = require("fs");
// Create a server
const server = http.createServer((req, res) => {
// Reading a file (I/O operation)
fs.readFile("large-file.txt", (err, data) => {
if (err) {
// If there's an error, this callback might be deferred to pending callbacks phase. (To be executed for the next iteration!)
console.error("Error reading file:", err);
res.statusCode = 500;
res.end("Server error");
return;
}
res.statusCode = 200;
res.end(data);
});
});
// If there's an error with TCP connection
server.on("error", (err) => {
// This callback will likely be processed in the pending callbacks phase
console.error("Server error:", err);
});
server.listen(3000, () => {
console.log("Server running on port 3000");
});
In this example, if there's a TCP error in the server, or if the file reading operation encounters issues, those error callbacks might be processed during the pending callbacks phase rather than immediately.
在此示例中,如果服务器中存在 TCP 错误,或者文件读取作遇到问题,则可能会在 pending callbacks 阶段处理这些错误回调,而不是立即处理。
Here is a takeout you should take from this phase:
以下是您应该从这个阶段中得出的结论:
If an error happens, that error's callback function won't be executed until a later time. (Until the next iteration's pending callback phase)
如果发生错误,该错误的回调函数将在稍后才会执行。(直到下一次迭代的 pending 回调阶段)
But what if we processed errors immediately instead of deferring them? That's an excellent question about the design choices in Node.js.
If we tried to handle all errors immediately in their original phase (like the poll phase) instead of deferring some to the pending callbacks phase, several important consequences would occur:
但是,如果我们立即处理错误而不是推迟它们呢?这是关于 Node.js 中设计选择的一个很好的问题。如果我们试图在原始阶段(如 poll 阶段)立即处理所有错误,而不是将一些错误推迟到待处理的回调阶段,将发生几个重要的后果:
- Blocking the Event Loop: The most significant issue would be potential blocking of the event loop. Some system-level errors and callbacks might require substantial processing time. By handling everything immediately, the event loop could get stuck processing these operations, preventing it from moving on to other tasks.
阻塞事件循环:最重要的问题是事件循环的潜在阻塞。某些系统级错误和回调可能需要大量的处理时间。通过立即处理所有内容,事件循环可能会卡在处理这些作,从而阻止它继续执行其他任务。 - I/O Starvation: During the poll phase - which you will learn in detail about what poll phase is in a minute - Node.js is primarily focused on waiting for and handling new I/O events. If complex error handling occurred here, it could delay the processing of other incoming connections or events, potentially causing timeouts or dropped connections.
I/O 匮乏:在轮询阶段(您将在一分钟内详细了解什么是轮询阶段),Node.js 主要专注于等待和处理新的 I/O 事件。如果此处发生复杂的错误处理,则可能会延迟其他传入连接或事件的处理,从而导致超时或连接断开。
But remember, when we defer a callback to the pending callbacks phase, we're not magically eliminating the processing time it requires - we're just moving it to a different phase of the event loop. So why is this helpful? Because it can still block the event loop when the next iteration happens, right?
但请记住,当我们将回调推迟到 pending callbacks 阶段时,我们并没有神奇地消除它所需的处理时间 - 我们只是将它移动到事件循环的不同阶段。那么为什么这有帮助呢?因为它仍然可以在下一次迭代发生时阻止事件循环,对吧?
By moving certain callbacks to the pending callbacks phase, Node.js gains control over when these potentially heavy operations occur within the event loop cycle. This creates a more predictable performance pattern. The pending callbacks phase is strategically placed after timers but before the poll phase, which means:
通过将某些回调移至 pending callbacks 阶段,Node.js 可以控制这些可能繁重的作在事件循环周期内发生的时间。这将创建更可预测的性能模式。待处理回调阶段战略性地放置在计时器之后,但在轮询阶段之前,这意味着:
- Time-sensitive timer callbacks get executed first
首先执行对时间敏感的计时器回调 - Heavy system callbacks run before returning to I/O polling
在返回 I/O 轮询之前运行繁重的系统回调
Node.js and libuv - library that powers Node.js' event loop and provides cross-platform access to asynchronous I/O operations - apply limits to how many pending callbacks are processed in each iteration. If there are too many, some will be deferred to the next iteration. This is crucial because:
Node.js 和 libuv - 为 Node.js 事件循环提供支持并提供对异步 I/O 作的跨平台访问的库 - 对每次迭代中处理的待处理回调数量应用限制。如果太多,则有些将被推迟到下一次迭代。这至关重要,因为:
// Internally, libuv might do something like:
while (pendingQueue.length > 0 && processedCallbacks < MAX_CALLBACKS_PER_ITERATION) {
const callback = pendingQueue.shift();
callback();
processedCallbacks++;
}
This prevents the pending callbacks phase from completely dominating a single iteration of the event loop.
这可以防止 pending callbacks 阶段完全主导事件循环的单个迭代。
So every phase has some kind of unique control to prevent blocking behavior for the event loop. If "some operations" can be in the "right phase", then it makes everything much more manageable.
因此,每个阶段都有某种独特的控制,以防止事件循环的阻塞行为。如果 “某些作” 可以处于 “正确的阶段”,那么它使一切都更容易管理。
Note: There is an exception for some cases. When using Node.js to establish TCP connections, the order in which you see success and error callbacks can sometimes be unexpected due to how the event loop handles these operations. Error callbacks might execute before success callbacks if:
注意:某些情况有例外。使用 Node.js 建立 TCP 连接时,由于事件循环处理这些作的方式,您看到成功和错误回调的顺序有时可能会出乎意料。在以下情况下,错误回调可能会在成功回调之前执行:
Failed connections often fail quickly - if a server doesn't exist or a port is closed, the system can determine this almost immediately. So in this case, you might not see error callback after success callback as you might have expected. Since it fails so quickly, the event loop won't register it to the pending callbacks phase and instead it will just run that callback immediately.
失败的连接通常会很快失败 - 如果服务器不存在或端口关闭,系统几乎可以立即确定这一点。因此,在这种情况下,您可能不会像预期的那样在成功回调后看到错误回调。由于它失败得太快了,事件循环不会将其注册到 pending callbacks 阶段,而是立即运行该回调。
On the other hand, there are several situations where error callbacks might appear later (after success callbacks) when working with TCP connections in Node.js, and one of them is Timeout-based failures:
另一方面,在以下几种情况下,在 Node.js 中使用 TCP 连接时(成功回调之后)可能会出现错误回调,其中一种是基于超时的失败:
When a connection attempt doesn't receive any response, it may need to wait for the timeout period (which could be several seconds) before triggering the error callback. In these cases, you will see success callbacks trigger before error callbacks, even if the error connection was initiated first in your code.
当连接尝试未收到任何响应时,它可能需要等待超时时间(可能几秒钟)才能触发错误回调。在这些情况下,您将看到成功回调在错误回调之前触发,即使错误连接是首先在代码中启动的。
Okay we've spent enough time on the pending callbacks phase. Now, let's deep dive into another phase which is called Idle, prepare.
好的,我们已经在 pending callbacks 阶段花了足够的时间。现在,让我们深入研究另一个称为 Idle, prepare 的阶段。
Idle, prepare phase 空闲、准备阶段
This is the phase right after pending callbacks, our third (fourth overall) phase in the event loop. This phase is an internal phase for Node. Not for us users actually.
这是 pending callbacks 之后的阶段,也是事件循环中的第三个(总体第四个)阶段。此阶段是 Node 的内部阶段。实际上不是为了我们用户。
So we have 2 different steps in one phase. When it comes to idle, there is not much we can talk about actually. Like I said, it's for Node's internal tasks. And of course, it runs in every iteration.
所以我们在一个阶段有 2 个不同的步骤。说到闲置,我们实际上可以谈论的不多。就像我说的,它用于 Node 的内部任务。当然,它在每次迭代中都会运行。
But for the prepare step, it is an important step in Node.js's event loop that occurs immediately after the Idle step and before the Poll phase. During this step, the event loop prepares for potential upcoming events and executes certain types of scheduled callbacks.
但对于 prepare 步骤,它是 Node.js 事件循环中的一个重要步骤,它发生在 Idle 步骤之后和 Poll 阶段之前。在此步骤中,事件循环为可能即将发生的事件做准备,并执行某些类型的计划回调。
The main purpose of the Prepare step is to execute callbacks that need to run before the Poll phase starts polling for I/O events. This is a strategic moment in the event loop cycle when Node.js can perform necessary setup operations before potentially blocking for I/O in the upcoming Poll phase.
Prepare 步骤的主要目的是执行需要在 Poll 阶段开始轮询 I/O 事件之前运行的回调。这是事件循环周期中的一个战略时刻,此时 Node.js 可以在即将到来的 Poll 阶段可能阻塞 I/O 之前执行必要的设置作。
These callbacks are not your own callbacks, but ones used by Node.js to get things ready. For example, it may set up timers or prepare network events. This phase helps make sure everything is in place before the event loop starts polling for new activity.
这些回调不是你自己的回调,而是 Node.js 用来准备工作的回调。例如,它可以设置计时器或准备网络事件。此阶段有助于确保在事件循环开始轮询新活动之前一切就绪。
For example, let's say you spin up a TCP server.
例如,假设您启动了一个 TCP 服务器。
const net = require('net');
const server = net.createServer((socket) => {
socket.end('Hello world\n');
});
server.listen(3000);
Before the event loop enters the poll phase, Node.js may run some internal setup code during the prepare phase. This setup ensures the server is ready to accept new connections. These are not user-defined callbacks, but internal operations handled by Node.js to manage the underlying system behavior.
在事件循环进入轮询阶段之前,Node.js prepare 阶段可能会运行一些内部设置代码。此设置可确保服务器已准备好接受新连接。这些不是用户定义的回调,而是由 Node.js 处理的内部作,用于管理底层系统行为。
Now, let's go into the phase right after idle, prepare. Our fourth (fifth overall) phase in the event loop.
现在,让我们进入 idle 之后的阶段,做好准备。我们在事件循环中的第四个(总体第五个)阶段。
Poll phase 轮询阶段
This phase is called the poll phase, and this phase is probably the most important phase in our event loop.
这个阶段称为轮询阶段,这个阶段可能是我们事件循环中最重要的阶段。
Two important things happen during this phase:
在此阶段会发生两件重要的事情:
-
Node.js checks for I/O events Think of it like this: Node.js constantly asks, "Is there anything new?" Similar to a waiter walking around a restaurant asking, "Do you need anything?" Node.js, as a waiter, checks for:
Node.js 检查 I/O 事件 可以这样想:Node.js 不断询问:“有什么新内容吗?类似于服务员在餐厅里走来走去问:“你需要什么吗?Node.js 作为服务员,会检查:- Any incoming network requests
任何传入的网络请求 - Any completed file reading/writing operations
任何已完成的文件读/写作 - Any new connection requests
任何新的连接请求
For example, when a user visits your website or when you want to read a file, Node.js sends these requests to the operating system and waits for results. The poll phase is where Node.js checks for these results.
例如,当用户访问您的网站或您想要阅读文件时,Node.js 会将这些请求发送到作系统并等待结果。轮询阶段是 Node.js 检查这些结果的地方。 - Any incoming network requests
-
Callbacks for completed operations are executed When an operation finishes (like a file being read or a network request arriving), Node.js runs the function (callback) associated with that operation. For example:
执行已完成作的回调 当作完成时(如读取文件或网络请求到达),Node.js 将运行与该作关联的函数 (callback)。例如:onRead
: When a file finishes reading, we might log something to the console
onRead
: 当文件完成读取时,我们可能会将一些内容记录到控制台onConnected
: When a connection is established, we perform a specific action
onConnected
:建立连接后,我们执行特定作onListen
: When a socket begins listening, we execute particular code
onListen
: 当套接字开始侦听时,我们执行特定的代码
Dynamic imports (using import()) are also processed during this phase.
动态导入(使用 import())也会在此阶段进行处理。
There's something crucial you should understand: Node.js can actually block in the poll phase. This means the event loop might pause here waiting for I/O events.
您应该了解一些至关重要的事情:Node.js 实际上可以在轮询阶段阻止。这意味着事件循环可能会在此处暂停,等待 I/O 事件。
When your Node.js application has nothing else to do, it will stay in the poll phase, waiting for I/O callbacks to come in. The poll phase will block (wait) until either:
当您的 Node.js 应用程序无事可做时,它将保持在轮询阶段,等待 I/O 回调进入。轮询阶段将阻塞(等待),直到:
- An I/O event triggers its callback
I/O 事件触发其回调 - A timer (from the timers phase) becomes due
计时器(从 timers 阶段)变为 due - There are setImmediate callbacks waiting to be executed
有 setImmediate 回调等待执行
This blocking behavior is intentional and allows Node.js to efficiently wait for work to arrive rather than constantly spinning through the event loop phases when nothing needs to be done. (You will have a better understanding when you read the next phase, which is check phase. So bear with me!)
这种阻塞行为是有意为之的,它允许 Node.js 有效地等待工作到达,而不是在不需要做任何事情时不断地旋转事件循环阶段。(当您阅读下一阶段,即检查阶段时,您将有更好的理解。所以请耐心等待!
While Node.js blocks in the poll phase, it's important to understand this doesn't mean your entire application is frozen. This is "good blocking" - efficiently waiting for the next task.
虽然 poll 阶段 Node.js 块,但重要的是要明白这并不意味着你的整个应用程序都被冻结了。这就是 “good blocking” - 有效地等待下一个任务。
However, you should be careful with CPU-intensive operations during callbacks executed in this phase. For example:
但是,在此阶段执行回调期间,您应该小心 CPU 密集型作。例如:
// This could block the entire event loop if the file is large
fs.readFile('large-file.txt', (err, data) => {
// CPU-intensive operation in the callback
const result = performComplexCalculation(data);
console.log(result);
});
Note: For CPU-intensive tasks, we can consider using:
注意:对于 CPU 密集型任务,我们可以考虑使用:
- Worker threads - It's not in the context of this blog, but you can try to google it and learn. -
Worker 线程 - 它不在本博客的上下文中,但您可以尝试用 google 搜索并学习。-- Child processes - It's not in the context of this blog, but you can try to google it and learn. -
子进程 - 它不在本博客的上下文中,但您可以尝试用 google 搜索它并学习。-- Breaking work into smaller chunks using
setImmediate()
-setImmediate()
is a function that you will learn in the next phase! -
使用setImmediate()
-setImmediate()
将工作分成更小的块是您将在下一阶段学习的函数!-
So the poll phase is where Node.js spends most of its time when your application is handling I/O operations like:
因此,轮询阶段是应用程序处理 I/O 作时 Node.js 花费大部分时间的地方,例如:
- Handling HTTP requests 处理 HTTP 请求
- Reading/writing database data
读/写数据库数据 - Processing file operations
处理文件作
Now, let's go into our next phase which is the Check Phase.
现在,让我们进入下一个阶段,即检查阶段。
Check Phase 检查阶段
This phase, unlike idle and prepare, is a phase that we have "control" over as users. We can schedule callbacks to be executed exactly in this phase. Right after the poll phase, so right after the I/O operations.
与 idle 和 prepare 不同,此阶段是我们作为用户可以 “控制” 的阶段。我们可以安排 callbacks 在这个阶段完全执行。紧接在 poll 阶段之后,所以紧接在 I/O 作之后。
setImmediate
is the function to schedule check phase callbacks. We use it for deterministic outcomes. Because we know that this function will be executed right after the poll phase.
setImmediate
是调度 Check 阶段回调的函数。我们将其用于确定性结果。因为我们知道这个函数会在 poll 阶段之后立即执行。
It's okay if you don't fully get it, because now, you will after this example.
如果你没有完全理解也没关系,因为现在,你会在这个例子之后。
Let's say we have the following code:
假设我们有以下代码:
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Let's analyze what happens when we run this code.
我们来分析一下运行此代码时会发生什么。
First, we start with the initial phase, right? We register everything in the initial phase but we don't read the file or execute the callback yet! Because it's poll phase's responsibility! So we've scheduled these events (read the file, and execute the callback) for the poll phase.
首先,我们从初始阶段开始,对吧?我们在初始阶段注册了所有内容,但还没有读取文件或执行回调!因为这是投票阶段的责任!因此,我们将这些事件(读取文件并执行回调)安排在 poll 阶段。
We are still in the initial phase! We aren't done reading our code and scheduling things for the appropriate phases.
我们仍处于起步阶段!我们还没有完成阅读代码并为适当的阶段安排事情。
Now, our initial phase saw setImmediate() function, what is it going to do? It will schedule this callback for the check
phase.
现在,我们的初始阶段看到了 setImmediate() 函数,它将做什么?它将为 check
阶段安排此回调。
Now, the initial phase is done. Then we go to timer phase, nothing happens as there are no timers. Then we go to pending callbacks, is there anything to do? No, then move on. Then idle, prepare. Node.js may do some internal work but it's not our job. Then, finally move to the poll phase.
现在,初始阶段已完成。然后我们进入计时器阶段,由于没有计时器,所以什么也没发生。然后我们转到 pending callbacks,有什么可做的吗?不,那就继续吧。然后闲置,准备。Node.js 可能会做一些内部工作,但这不是我们的工作。然后,最后进入轮询阶段。
There are 2 things to do in the poll phase. First thing is, we need to read the file, and we need to execute the callback, right? Before we have to execute the callback, we have to issue the read operation. After we issued the file read, now Node.js will immediately move on to the next phase because there is a setImmediate()
function that is waiting to be executed in the check
phase. Because Node.js doesn't want to spend time and wait (block) the event loop for both read and execute operations.
在投票阶段有 2 件事要做。首先,我们需要读取文件,并且需要执行回调,对吧?在我们必须执行回调之前,我们必须发出读取作。在我们发出文件读取后,现在 Node.js 将立即进入下一阶段,因为在 check
阶段有一个 setImmediate()
函数正在等待执行。因为 Node.js 不想花费时间等待 (阻止) 事件循环进行读取和执行作。
So it will move to the check phase, it will print setImmediate called
to the console, and loop all the way back to the timers phase, and poll phase again. By the time we come back to the poll phase again, let's assume the read process has completed. So we will execute the poll phase's callback now, in the second loop.
所以它会进入检查阶段,它会将 setImmediate called
打印到控制台,然后一直循环回到 timers 阶段,然后再次返回 poll 阶段。当我们再次回到轮询阶段时,我们假设读取过程已经完成。因此,我们现在将在第二个循环中执行 poll 阶段的回调。
Here's what actually happens:
以下是实际发生的情况:
Node.js enters the poll phase.
It checks with the operating system: "What I/O operations have completed?".
For each completed operation, it executes the associated callback right away.
If there are no callbacks waiting in the poll phase, BUT:
If there are setImmediate callbacks, it exits poll phase to handle them!
Node.js 进入轮询阶段。它与作系统一起检查:“哪些 I/O 作已完成?对于每个已完成的作,它会立即执行关联的回调。如果在 poll 阶段没有等待的回调,但是:如果有 setImmediate 回调,它将退出 poll 阶段来处理它们!
So, while Node.js does two main things in the poll phase (checks for completed I/O events and executes callbacks for those completed events), it doesn't continuously wait in the poll phase for each individual I/O operation to complete. Instead, it queries the operating system for any I/O operations that have already completed since the last check.
因此,虽然 Node.js 在轮询阶段执行两项主要作(检查已完成的 I/O 事件和执行这些已完成事件的回调),但它不会在轮询阶段持续等待每个单独的 I/O 作完成。相反,它会在作系统中查询自上次检查以来已完成的任何 I/O 作。
There is one important thing you should know! If the file didn't exist, that means we would not be able to issue the file read, and in that particular case, our callback function would be executed immediately. So the setImmediate()
function wouldn't be executed first!
But this all makes sense. Because if the file doesn't exist, then there will be no "heavy" reading operation so we are not going to block the event loop anyway. That's why there is no point to go to the check
phase first.
有一件重要的事情你应该知道!如果文件不存在,这意味着我们将无法发出文件读取,在这种特殊情况下,我们的回调函数将立即执行。所以 setImmediate()
函数不会先执行!但这一切都是有道理的。因为如果文件不存在,那么就不会有 “heavy” 读取作,所以无论如何我们都不会阻塞事件循环。这就是为什么没有意义先进入 check
阶段。
So try the code below, and see for yourself how the outputs will look like!
因此,请尝试下面的代码,亲眼看看输出会是什么样子!
test.txt
file exists: test.txt
文件存在:
const fs = require(`fs`);
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Output will be:
setImmediate called!
readFileCallback
输出将为: setImmediate called!
readFileCallback
test123.txt
file does not exist:
test123.txt
文件不存在:
const fs = require(`fs`);
const readFileCallback = (err, data) => {
console.log(`readFileCallback ${data}`);
};
const f = "test123.txt";
fs.readFile(f, readFileCallback);
setImmediate(() => console.log("setImmediate called!"));
Output will be:
readFileCallback undefined
setImmediate called!
输出将为: readFileCallback undefined
setImmediate called!
Now, we move to our final phase! Which is the Close Callbacks phase.
现在,我们进入最后阶段!这是 Close Callbacks 阶段。
Close Callbacks Phase Close 回调阶段
After Node.js has handled all the timers, I/O callbacks, idle checks, and immediate callbacks, there's one final stop in the event loop: the close callback phase.
在 Node.js 处理完所有计时器、I/O 回调、空闲检查和立即回调后,事件循环中还有最后一个停止:close 回调阶段。
Think of Close Callbacks as Node's way of doing final cleanup before it's done processing. When you close a server or a socket connection, Node.js needs to make sure everything is properly shut down.
将 Close Callbacks 视为 Node 在完成处理之前进行最终清理的方式。当您关闭服务器或套接字连接时,Node.js 需要确保所有内容都已正确关闭。
The Close Callbacks phase specifically handles callbacks that are triggered by events like:
Close Callbacks 阶段专门处理由以下事件触发的回调:
- A TCP server closing with
server.close()
以 @0 结束的 TCP 服务器# - A socket connection ending with
socket.on('close', ...)
以 @0 结尾的套接字连接# - A process exiting normally
正常退出的进程
Let's say you have a net.Socket
, and you attach a .on('close', ...)
listener to it. That function doesn't run right away when the socket closes. It waits for this special phase to fire.
假设您有一个 net.Socket
,并且您向其附加了一个 .on('close', ...)
侦听器。当套接字关闭时,该函数不会立即运行。它等待这个特殊阶段触发。
Let's see in an example.
让我们看一个例子。
const net = require("net");
const server = net.createServer((socket) => {
console.log("Client connected");
socket.on("close", () => {
console.log("Socket closed – this runs in the close callback phase");
});
// Close the socket after 2 seconds
setTimeout(() => {
socket.end(); // Triggers 'close'
}, 2000);
});
server.listen(3000, () => {
console.log("Server listening on port 3000");
});
What's Happening Here? 这里发生了什么?
- A client connects to the server.
客户端连接到服务器。 - After 2 seconds, the server ends the socket connection.
2 秒后,服务器结束套接字连接。 - When that happens, the 'close' event is emitted.
发生这种情况时,将发出 'close' 事件。 - Node waits until it reaches the close callback phase of the event loop to actually run the callback for
.on('close', ...)
.
Node 等待到达事件循环的 close 回调阶段,然后实际运行.on('close', ...)
的回调。
So that final console.log won't run immediately after .end()
, it waits for Node to reach that phase.
因此,最终 console.log 不会在 .end()
之后立即运行,它会等待 Node 到达该阶段。
Now, what I want you to do is, go back to the Phases Overview section, right before we started to dive deep into phases, and read it again. Now, you will see that you have a clear understanding about what happens in every phase.
现在,我希望您做的是,在我们开始深入研究 Phases 之前,返回到 Phases Overview 部分,然后再次阅读它。现在,您将看到您对每个阶段发生的事情都有清晰的了解。
We completed every phase but there is only one thing left to complete our event loop. And it's a phase that we've never talked about so far. It's like a "Hidden phase", and yes, we're talking about the process.nextTick()
.
我们完成了每个阶段,但只剩下一件事来完成我们的事件循环。到目前为止,我们从未讨论过这个阶段。这就像一个 “隐藏阶段”,是的,我们谈论的是 process.nextTick()
。
As you see in the image above, process.nextTick()
is executed after every phase! Sounds crazy, right? So the actual event loop is something like timers -> process.nextTick()
-> pending callbacks -> process.nextTick()
...
如上图所示, process.nextTick()
在每个阶段后执行!听起来很疯狂,对吧?所以实际的事件循环类似于 timers -> process.nextTick()
-> pending callbacks -> process.nextTick()
...
Actually it starts even before timers. Because before the event loop, we have the initial phase. process.nextTick()
will work right after the initial phase too!
实际上,它甚至在计时器之前就开始了。因为在事件循环之前,我们有一个初始阶段。 process.nextTick()
也会在初始阶段后立即工作!
You may have noticed that process.nextTick()
was not displayed in the first event loop diagram, even though it's a part of the asynchronous API. This is because process.nextTick()
is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
您可能已经注意到, process.nextTick()
没有显示在第一个事件循环图中,即使它是异步 API 的一部分。这是因为 process.nextTick()
在技术上不是事件循环的一部分。相反,nextTickQueue 将在当前作完成后进行处理,而不管事件循环的当前阶段如何。
process.nextTick()
is actually very powerful. If you remember, before we go into the event loop, we have the "initial phase -main module-" right? After the initial phase is completed, only then do we start the event loop. So NextTick can tell us when the initial phase actually ends! We could set timers to see when the event loop started, -since timers are the first phase of the event loop- but timers can get delayed, so NextTick will give us more accurate results. It can be a good indicator to know when the initial phase really ends, and when the event loop is about to start.
process.nextTick()
其实非常强大。如果你还记得,在我们进入事件循环之前,我们有 “initial phase -main module-” 对吧?在初始阶段完成后,我们才会启动事件循环。因此,NextTick 可以告诉我们初始阶段何时真正结束!我们可以设置计时器来查看事件循环何时开始,因为计时器是事件循环的第一阶段,但计时器可能会延迟,因此 NextTick 会给我们更准确的结果。了解初始阶段何时真正结束以及事件循环何时即将开始,这可能是一个很好的指标。
Now, let's see some examples.
现在,让我们看看一些示例。
console.log("start");
for (let i = 0; i < 1000000; i++);
console.log("end");
setTimeout(() => console.log("timer"), 0);
process.nextTick(() => console.log("nextTick"));
So, what will be the output?
那么,输出会是什么呢?
start
end
nextTick
timer
Why? Because right after the initial phase, NextTick will run, and only then will the event loop -so the timer phase- start!
为什么?因为在初始阶段之后,NextTick 将运行,只有这样事件才会循环 - 所以计时器阶段 - 开始!
Now, let's see the example below, and once again we will understand the power of Node.js.
现在,让我们看看下面的示例,我们将再次了解 Node.js 的强大功能。
let val;
function test() {
console.log(val);
}
test();
val = 1;
So, all the code is synchronous here. Nothing hard to understand. First, we initialize the variable val
, but it's undefined. Then, before we say val=1
, we call the function and that will print the val
, but at that time in the code, val
is undefined. So we will see undefined
in the console. But what if we can fix it in a very cool way?
所以,这里的所有代码都是同步的。没什么难懂的。首先,我们初始化变量 val
,但它是 undefined。然后,在我们说 val=1
之前,我们调用函数,它将打印 val
,但在代码中, val
是未定义的。因此,我们将在控制台中看到 undefined
。但是,如果我们能以一种非常酷的方式修复它呢?
let val;
function test() {
console.log(val);
}
process.nextTick(test);
val = 1;
Now, what do you think will happen? process.nextTick() won't be executed within the initial phase, but it will get executed right after the initial phase! So, it will be executed after we assign value 1 to it. So this time, even though code placement is completely the same, now we see 1
in the console!
现在,您认为会发生什么?process.nextTick() 不会在初始阶段执行,但它会在初始阶段之后立即执行!因此,它将在我们为其赋值 1 后执行。所以这一次,尽管代码放置完全一样,但现在我们在控制台看到了 1
!
There are two main reasons to use process.nextTick()
:
使用 process.nextTick()
有两个主要原因:
- Allow users to handle errors, cleanup any then unneeded resources, or perhaps try the request again before the event loop continues.
允许用户处理错误、清理任何不需要的资源,或者在事件循环继续之前再次尝试请求。 - At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.
有时,有必要允许回调在调用堆栈展开之后但在事件循环继续之前运行。
One last thing you should know - I promise you, it's the last one - Promises in Node.js operate within the microtask queue of the event loop, but with a lower priority than process.nextTick callbacks. When a Promise is resolved, its .then() handlers are queued as microtasks to be executed after all nextTick callbacks have completed, but before the event loop moves on to the next phase like timers or I/O operations. This prioritization ensures that Promise chains execute in a predictable order while maintaining the non-blocking nature of Node.js.
你应该知道的最后一件事 - 我向你保证,这是最后一件事 - Node.js 中的 Promise 在事件循环的微任务队列中运行,但优先级低于 process.nextTick 回调。当 Promise 被解析时,它的 .then() 处理程序将作为微任务排队,在所有 nextTick 回调完成后执行,但在事件循环进入下一阶段(如计时器或 I/O 作)之前执行。这种优先级可确保 Promise 链以可预测的顺序执行,同时保持 Node.js 的非阻塞性质。
If you know How Javascript Works Behind The Scenes in a web environment, in terms of Node.js equivalents:
如果你知道 Javascript 在 Web 环境中的幕后是如何工作的,就 Node.js 等价物而言:
Browser's macrotask queue → Distributed across different phases in Node.js
Browser's microtask queue → Used for Promise callbacks and process.nextTick() in Node.js
浏览器的宏任务队列 → 分布在 Node.js 浏览器的微任务队列中→用于 Promise 回调和 process.nextTick() Node.js
An important difference in Node.js: process.nextTick() operations are processed before other Promises in the microtask queue and are checked at the end of each phase.
Node.js 的一个重要区别是: process.nextTick()作在微任务队列中的其他 Promise 之前处理,并在每个阶段结束时进行检查。
I think now you fully understand how Node.js actually works behind the scenes!
我想现在你完全明白了 Node.js 幕后的实际运作方式!
Was this blog helpful for you? If so,
这个博客对您有帮助吗?㞖