The Node.js Event Loop, Timers, and process.nextTick() Node.js事件循环,定时器和process.nextTick()

时间:2021-08-07 15:30:59

个人翻译

原文:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

The Node.js Event Loop, Timers, and process.nextTick()

What is the Event Loop?

什么是事件循环圈?

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed. We'll explain this in further detail later in this topic.

事件循环允许了nodejs来实现非阻塞I/O操作——尽管JavaScript是单线程的——通过向操作系统内核(分流)操作。

大多数现代的内核都是多线程的,他们能在后台处理多线程操作。当某一个操作完成的时候,内核通知Nodejs,因此合适的回掉函数会被放到轮询队列中最终被执行,我们会在这篇文章的后面部分详细的解释这些内容。

Event Loop Explained

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

The following diagram shows a simplified overview of the event loop's order of operations.

事件循环的解释:

当Node.js运行的时候,它会初始化事件循环圈,处理当前的脚本(或者是在REPL中,这篇文章不涉及此方面内容),可能会调用一些异步的API、定时器、或者调用process.nextTick(),然后开始处理event loop,

下面图表展示了事件循环中各种操作顺序的一个简单概况。

   ┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘

note: each box will be referred to as a "phase" of the event loop.

注意:每个盒子都代表了事件循环的一个阶段。

Each phase has a FIFO 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.

每个阶段都要执行一个先进先出的队列的回调。每个阶段都是唯一特殊的,一般来说,当event loop进入了某个阶段,它会执行那个阶段的所有操作,然后执行那个阶段队列里的回调直到队列结束,或者执行了最大限制值的回调函数数量。当队列结束或者回调数量限制达到了,event loop会进入下一个阶段,这样。

Since any of these operations may schedule more operations and new events processed in the poll phase are queued by the kernel, poll events can be queued while polling events are being processed. As a result, long running callbacks can allow the poll phase to run much longer than a timer's threshold. See the timers and poll sections for more details.

既然这些操作会安排更多的操作,并且在 poll(轮询)阶段 处理的新的事件会被操作系统内核顺序地队列,当处理轮询事件时,其它的poll(轮询)事件会被队列。结果是执行时间较长的回调会允许 poll阶段 比timer的阈值运行的更长。 下面部分会详细介绍timers阶段和poll阶段。

NOTE: There is a slight discrepancy between the Windows and the Unix/Linux implementation, but that's not important for this demonstration. The most important parts are here. There are actually seven or eight steps, but the ones we care about — ones that Node.js actually uses - are those above.

注意:在windows和Linux的实现上有微小的差别,但是在这里的演示中并不重要,这里列出了最重要的部分,实际上有7、8步,但是这里展示出的是我们关心的——展示出的也是Node.js实际使用的这些。

Phases Overview

阶段总览

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval(). 这个阶段执行由setTimeout()和setInterval的组织的callback。
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration. 执行推迟到下一个事件循环的I/O 回调
  • idle, prepare: only used internally. 在内部使用
  • poll: retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will block here when appropriate.
  • 获得新的I/O事件,执行I/O相关的回调(除了”close回调“的几乎所有的回调,由timers和setImmediate()组织的)  Node会在这儿适当的阻塞。
  • check: setImmediate() callbacks are invoked here.  setTimmediate()回调再这里被调用。
  • close callbacks: some close callbacks, e.g. socket.on('close', ...). 一些负责关闭的回调,如socket.on('close')..

Between each run of the event loop, Node.js checks if it is waiting for any asynchronous I/O or timers and shuts down cleanly if there are not any.

在每次Event loop循环中,Node.js会检查,是否任何异步I/O或者timers都被干净的关闭掉了。

Phases in Detail

阶段的详细概述

timers

A timer specifies the threshold after which a provided callback may be executed rather than the exact time a person wants it to be executed. Timers callbacks will run as early as they can be scheduled after the specified amount of time has passed; however, Operating System scheduling or the running of other callbacks may delay them.

timer,一个timer明确了一个临界值,在这个临界值后callback可能会被执行,而不是这个临界值之后这个callback会立刻执行。timer的回调函数会在特定的时间之后尽快的运行,因为操作系统的时序安排或者其它回调函数的运行可能会引起延迟。

Note: Technically, the poll phase controls when timers are executed.

注意:技术上来说,poll阶段控制着什么时候timers会被运行

For example, say you schedule a timeout to execute after a 100 ms threshold, then your script starts asynchronously reading a file which takes 95 ms:

举个例子,你打算在100ms后执行,你的脚本开始异步的读取一个文件花95ms:

const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
} const timeoutScheduled = Date.now(); setTimeout(() => {
const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`);
}, 100); // do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now(); // do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

When the event loop enters the poll phase, it has an empty queue (fs.readFile() has not completed), so it will wait for the number of ms remaining until the soonest timer's threshold is reached. While it is waiting 95 ms pass, fs.readFile() finishes reading the file and its callback which takes 10 ms to complete is added to the poll queue and executed. When the callback finishes, there are no more callbacks in the queue, so the event loop will see that the threshold of the soonest timer has been reached then wrap back to the timers phase to execute the timer's callback. In this example, you will see that the total delay between the timer being scheduled and its callback being executed will be 105ms.

当事件循环进入poll阶段,有一个空队列(fs.readFile还没完成),所以它会等待直到最快的timers值达到,当到95ms的时候,fs.readFile()完成了读取文件并且它的回调花10ms来完成,它被加到poll阶并被执行,当回调完成了,队列中就没有更多的回调了,所以事件循环会。。 在这个例子中,你会发现从timer开始到timer的回调被执行总的延迟是105ms

Note: To prevent the poll phase from starving the event loop, libuv (the C library that implements the Node.js event loop and all of the asynchronous behaviors of the platform) also has a hard maximum (system dependent) before it stops polling for more events.

注意:为了阻止poll阶段饿死事件循环圈,libuv(实现node.js事件循环圈和平台上所有异步行为的C库)也有一个最大值(根据操作系统不同)   它阻止poll 为了更多的事件。

pending callbacks (等待回调阶段)

This phase executes callbacks for some system operations such as types of TCP errors. For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the pending callbacks phase.

这个阶段执行一些操作系统的操作的回调,比如说各种类型的TCP错误。比如说一个TCP socket 在尝试连接的时候收到了ECONNREFUSE,一些*nix 操作系统希望等待来报告错误,这些东西会被队列,来在pending callbacks阶段执行。

poll

The poll phase has two main functions:

poll阶段有两个主要的功能

  1. Calculating how long it should block and poll for I/O, then
  2. Processing events in the poll queue.

1.计算阻塞或者I/O poll需要多久,然后

2.处理poll队列中的事件

When the event loop enters the poll phase and there are no timers scheduled, one of two things will happen:

当事件循环进入poll阶段, 并且没有 被安排的timer时,下面两件其中之一的事会发生:

  • If the poll queue is not empty, the event loop will iterate through its queue of callbacks executing them synchronously until either the queue has been exhausted, or the system-dependent hard limit is reached.

      • 如果poll 队列没有空,事件循环会递归它的回调队列,并异步的执行他们,直到某队列的尽头,或者到达操作系统的最大限制值
  • If the poll queue is empty, one of two more things will happen:

  • 如果poll队列是空的,下面两件其中之一的事情会发生:

    • If scripts have been scheduled by setImmediate(), the event loop will end the pollphase and continue to the check phase to execute those scheduled scripts.

      • 如果脚本被setImmediate()安排了,事件循环会结束poll阶段并且
    • If scripts have not been scheduled by setImmediate(), the event loop will wait for callbacks to be added to the queue, then execute them immediately.

如果脚本没有被setImmediate()安排,事件循环会等待callback加到队列的结尾,然后立刻执行他们

Once the poll queue is empty the event loop will check for timers whose time thresholds have been reached. If one or more timers are ready, the event loop will wrap back to the timers phase to execute those timers' callbacks.

一旦poll阶段空了,事件循环会检查到达阈值的timer。如果一个或者更多的timer准备好了,事件循环会绕回到timer阶段来执行timer的callback

check

This phase allows a person to execute callbacks immediately after the poll phase has completed. If the poll phase becomes idle and scripts have been queued with setImmediate(), the event loop may continue to the check phase rather than waiting.

这个阶段允许一个人在poll阶段完成后立即执行回调。如果poll阶段闲置了并且脚本被setImmediate()队列了,事件循环会继续到check阶段,而不是等待

setImmediate() is actually a special timer that runs in a separate phase of the event loop. It uses a libuv API that schedules callbacks to execute after the poll phase has completed.

setImmediate()实际上是一个特殊的timer,在事件循环的一个独立的阶段运行,它使用了libuv API ,这些API组织的callback 在poll阶段完成后执行

Generally, as the code is executed, the event loop will eventually hit the poll phase where it will wait for an incoming connection, request, etc. However, if a callback has been scheduled with setImmediate() and the poll phase becomes idle, it will end and continue to the checkphase rather than waiting for poll events.

通常来说,当代码执行的时候,事件循环最终到达poll阶段,这个阶段会等待即将到来的connection,request...等等。然而,如果一个回调被setimmediate()组织了并且poll阶段闲置了,它会结束并且继续 check阶段,而不是等待poll的事件。

close callbacks

If a socket or handle is closed abruptly (e.g. socket.destroy()), the 'close' event will be emitted in this phase. Otherwise it will be emitted via process.nextTick().

setImmediate() vs setTimeout()

setImmediate and setTimeout() are similar, but behave in different ways depending on when they are called.

setImmediate和setTimeout是相似的,但是表现的不同,这种不同取决于什么时候被调用

  • setImmediate() is designed to execute a script once the current poll phase completes.
  • setImmediate()被设计来执行脚本,一旦当前poll阶段完成后执行。
  • setTimeout() schedules a script to be run after a minimum threshold in ms has elapsed.
  • setTimeout()组织的脚本在最小的时间阈值 消逝后执行

The order in which the timers are executed will vary depending on the context in which they are called. If both are called from within the main module, then timing will be bound by the performance of the process (which can be impacted by other applications running on the machine).

timer执行的顺序会根据  被调用的 被执行的上下文而变化。如果都是从main模块调用的,时间会根据处理的表现而波动。

(会被在机器上其它运行的程序而影响)

For example, if we run the following script which is not within an I/O cycle (i.e. the main module), the order in which the two timers are executed is non-deterministic, as it is bound by the performance of the process:

举例,如果我们运行下面脚本(不在I/O循环中,比如说main模块),这两个timer的运行顺序是不确定的,会根据处理的表现而不同

// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0); setImmediate(() => {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate $ node timeout_vs_immediate.js
immediate
timeout

However, if you move the two calls within an I/O cycle, the immediate callback is always executed first:

然而,如果你在一个I/O循环中,处理两个,immediate的回调总是先运行:

// timeout_vs_immediate.js
const fs = require('fs'); fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
$ node timeout_vs_immediate.js
immediate
timeout $ node timeout_vs_immediate.js
immediate
timeout

The main advantage to using setImmediate() over setTimeout() is setImmediate() will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.

使用setImmediate()而不是setTimeout()的主要优势是,如果在一个I/O循环中,setImmediate()总是会比任何timers先执行,与目前有多少个timer无关。

process.nextTick()

Understanding process.nextTick()

You may have noticed that process.nextTick() was not displayed in the 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 completes, regardless of the current phase of the event loop.

尽管process.nextTick()是异步的api,技术上来说process.nextTick不是时间循环圈的一部分, nextTickQueue 会在当前操作完成后被处理,而不管当前的事件循环阶段。

Looking back at our diagram, any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues. This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the pollphase.

在我们的图表中,任何阶段你都可以调用process.nextTick(),process.nextTick()的回掉会在事件循环继续之前被执行。会阻止时间循环到达pollphase阶段

Why would that be allowed?

Why would something like this be included in Node.js? Part of it is a design philosophy where an API should always be asynchronous even where it doesn't have to be. Take this code snippet for example:

为什么Node.js会发生这种情况?因为这个就是node的设计哲学——所有的API都该是异步的。用下面的代码片段来做个示例:

function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}

The snippet does an argument check and if it's not correct, it will pass the error to the callback. The API updated fairly recently to allow passing arguments to process.nextTick()allowing it to take any arguments passed after the callback to be propagated as the arguments to the callback so you don't have to nest functions.

这个代码段做了参数检查,并且如果不对的话,会把错误传递到callback中,这个API是最近更新的,来允许把参数传递到process.nextTick()中,如此,你就不用写嵌套函数了。

What we're doing is passing an error back to the user but only after we have allowed the rest of the user's code to execute. By using process.nextTick() we guarantee that apiCall()always runs its callback after the rest of the user's code and before the event loop is allowed to proceed. To achieve this, the JS call stack is allowed to unwind then immediately execute the provided callback which allows a person to make recursive calls to process.nextTick() without reaching a RangeError: Maximum call stack size exceeded from v8.

我们在做的是把错误传递回给用户,只是在我们允许用户的剩余代码执行的时候。通过process.nextTick()我们保证了 apiCall() 总是在用户代码之后,和eventloop重新跑之前得到执行。为了达到这个,JS调用栈允许立刻解开他们来执行提供的callback,callback允许一个人来执行递归到process.nextTick()而不reaching

RangeError:Maximum call stack size exceeded from v8.

This philosophy can lead to some potentially problematic situations. Take this snippet for example:

这个体系会引起一些潜在的问题,拿下面的代码段做个示例:

let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); } // the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
}); bar = 1;

The user defines someAsyncApiCall() to have an asynchronous signature, but it actually operates synchronously. When it is called, the callback provided to someAsyncApiCall() is called in the same phase of the event loop because someAsyncApiCall() doesn't actually do anything asynchronously. As a result, the callback tries to reference bar even though it may not have that variable in scope yet, because the script has not been able to run to completion.

用户定义了 someAsyncApiCall() 来实现一个异步的签名,但是它实际上是同步来执行的,当被调用的时候,回调提供的someAsyncApicall()在event loop的同一阶段被调用了,因为someAsyncApiCall() 没有异步的做任何事情,结果是,callback尝试引用bar尽管它的作用于内没有那个变量,因为脚本还不能运行。

By placing the callback in a process.nextTick(), the script still has the ability to run to completion, allowing all the variables, functions, etc., to be initialized prior to the callback being called. It also has the advantage of not allowing the event loop to continue. It may be useful for the user to be alerted to an error before the event loop is allowed to continue. Here is the previous example using process.nextTick():

通过在process.nextTick()放置回调,脚本仍然有运行的完成能力,允许所有的变量,函数...等等,在回调被调用之前初始化完成,它也有不让event loop来继续的优势,它对用户也可能是有用的通过alert一个错误在 时间循环继续之前,这儿是一个之前的使用process.nextTick()的例子:

let bar;

function someAsyncApiCall(callback) {
process.nextTick(callback);
} someAsyncApiCall(() => {
console.log('bar', bar); // 1
}); bar = 1;

Here's another real world example:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

When only a port is passed, the port is bound immediately. So, the 'listening' callback could be called immediately. The problem is that the .on('listening') callback will not have been set by that time.

To get around this, the 'listening' event is queued in a nextTick() to allow the script to run to completion. This allows the user to set any event handlers they want.

当一个port传递,之后,listening回调能被立刻的调用,问题是.on('listening')回调不会被立刻的设定。

为了达到,'listening'事件在nextTick()事件中排队了,来允许脚本运行完成。这允许用户设置任何他们想设置的事件handler。

process.nextTick() vs setImmediate()

We have two calls that are similar as far as users are concerned, but their names are confusing.

这儿有两个比较相似的函数。

  • process.nextTick() fires immediately on the same phase

process.nextTick() 在同一阶段立刻的执行

  • setImmediate() fires on the following iteration or 'tick' of the event loop

setImmediate()在接下来的迭代或event loop的tick中执行

In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate(), but this is an artifact of the past which is unlikely to change. Making this switch would break a large percentage of the packages on npm. Every day more new modules are being added, which means every day we wait, more potential breakages occur. While they are confusing, the names themselves won't change.

从本质上来说,名字应该被交换。process.nextTick()比setImmediate()执行的更快,但这是一个过去的约定,已经不太能改变了,改变这个东西会把npm上不少包给搞坏。每天更多的新包被加入进来,所以名字一直就没变。

We recommend developers use setImmediate() in all cases because it's easier to reason about (and it leads to code that's compatible with a wider variety of environments, like browser JS.)

我们建议开发者在任何情况下使用setImmediate(),因为它更容易搞明白。(并且它引起代码适应更大的环境范围)

Why use process.nextTick()?

为什么使用process.nextTick()?

There are two main reasons:

主要有两个原因:

  1. Allow users to handle errors, cleanup any then unneeded resources, or perhaps try the request again before the event loop continues.

允许用户处理错误,清理接下来不需要的资源,或者尝试在eventloop前重新请求。

  1. At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.

允许一个回调在函数调用栈展开之前运行。

One example is to match the user's expectations. Simple example:

一个例子是匹配用户的期望,一个简单的例子:

const server = net.createServer();
server.on('connection', (conn) => { }); server.listen(8080);
server.on('listening', () => { });

Say that listen() is run at the beginning of the event loop, but the listening callback is placed in a setImmediate(). Unless a hostname is passed, binding to the port will happen immediately. For the event loop to proceed, it must hit the poll phase, which means there is a non-zero chance that a connection could have been received allowing the connection event to be fired before the listening event.

listen() 是在事件循环的开头运行的,但是listening回调是在setImmediate()中放置的。除非一个hostname被传递了,则会立刻的绑定端口。为了事件循环的处理,必须进入到poll阶段,这意味着,这有一定的几率,在listening之前connection可能会接受允许connection事件运行

Another example is running a function constructor that was to, say, inherit from EventEmitter and it wanted to call an event within the constructor:

另一个例子是运行一个构造函数,继承自EventEmitter并且它想调用构造函数内的事件

const EventEmitter = require('events');
const util = require('util'); function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});

You can't emit an event from the constructor immediately because the script will not have processed to the point where the user assigns a callback to that event. So, within the constructor itself, you can use process.nextTick() to set a callback to emit the event after the constructor has finished, which provides the expected results:

你不能从构造函数立刻emit一个事件,因为这个脚本还没处理到那个点,where用户给那个事件分派一个callback。因此,在constructor内,你可以使用process.nextTick()来设置回调来emit事件在构造函数完成前,会提供预期的结果。

const EventEmitter = require('events');
const util = require('util'); function MyEmitter() {
EventEmitter.call(this); // use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});