js 多个定时器_[翻译]JavaScript 事件循环、定时器和 process.nextTick()(中英对照)

本文深入讲解了Node.js的事件循环机制,包括事件循环的基本概念、各个阶段的工作原理及其执行顺序。特别介绍了定时器、轮询阶段、即时回调等功能,并探讨了process.nextTick()与setImmediate()的区别。

97438976899770f0944857ebd48edbc8.png

Node.js 官方地址:

The Node.js Event Loop, Timers, and process.nextTick() | Node.js​nodejs.org
831d7137fe9ac00ccbc4f9113d8f8c50.png

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.

事件循环允许 Node.js 执行非阻塞 I/O 操作——尽管 JavaScript 是单线程的——只要有可能,就将操作卸载到系统内核完成。

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.

由于大多数现代内核都是多线程的,它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会告诉 Node.js,以便将适当的回调添加到轮询队列中,以最终执行。我们将在本章后面详细解释这一点。

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.

当 Node.js 启动时,它会初始化事件循环,处理提供的输入脚本(或放入 REPL,本文档中没有涉及到),这些脚本可能会进行异步 API 调用、调度计时器或调用process.nextTick(),然后开始处理事件循环。

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

下图显示了事件循环操作顺序的简化概述。

153439861cfd21f4d0a6440427d57252.png

注意:每个框对应事件循环的每个“阶段”。

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.

每个阶段都有一个先进先出(FIFO)队列来执行回调。虽然每个阶段都有自己的特殊之处,但通常情况下,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列执行完或已达到最大回调数限制。当队列耗尽或达到回调限制时,事件循环将进入下一个阶段,依此类推。

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.

由于这些操作中的任何一个都可能调度更多的操作,并且在轮询阶段处理的新事件都由内核排队,因此在轮询事件处理期间轮询事件可以排队。因此,长时间运行的回调会使轮询阶段的运行时间远远长于计时器的阈值。有关更多详细信息,请参阅计时器和轮询部分。

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 和 Unix/Linux 实现之间有一些细微的差异,但是这对于本演示来说并不重要。最重要的部分在这里。实际上有 7 到 8 个步骤,但我们关心的是 Node.js 实际使用的那些步骤。

Phases Overview 阶段概述

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • pending callbacks: executes I/O callbacks deferred to the next loop iteration.
  • 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.
  • check: setImmediate() callbacks are invoked here.
  • close callbacks: some close callbacks, e.g. socket.on('close', ...).
  • 定时器:这个阶段执行setTimeout()setInterval()的回调。
  • 挂起回调:执行下一循环迭代的 I/O 回调。
  • 闲置,准备:仅在内部使用。
  • 轮询:取得新的 I/O 事件,执行 I/O 相关回调(几乎所有的回调,除了 close 回调以及 timer 回调和setImmediate())node 可能在适当时候阻塞。
  • 检查setImmediate()将在这里调用。
  • 关闭回调:一些 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.

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

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.

定时器指定一个阈值,这个阈值代表着:在这个阈值之后,所提供的回调可能会被执行,而不是人们所希望的它被执行的确切时间。计时器回调将在指定时间之后尽可能早地运行;但是,操作系统调度或运行其他回调可能会延迟它们。

Note: Technically, the ref="https://http://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#poll">poll phase controls when timers are executed.

注意:从技术上讲,轮询阶段控制计时器何时执行。

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:

例如,假设您设置了一个 timeout 定时器,在 100 毫秒后执行回调,然后您的脚本开始异步读取一个文件,这需要 95 毫秒:

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.

(主线程执行,执行了计时器函数),当事件循环进入轮询阶段时,有一个空队列(fs.readFile()尚未完成)(轮询队列为空),因此它将等待剩余的 ms 数,直到最早的计时器阈值到达。在等待 95 ms 之后,fs.readFile()完成文件的读取(同步),并将需要 10 ms 才能完成的回调(异步)添加到轮询队列并执行。当回调结束时,队列中不再有回调,因此事件循环将看到最早的计时器阈值已经达到,然后返回到计时器阶段以执行计时器的回调。在本例中,您将看到从开始调度计时器到其回调开始执行之间的总延迟为 105 ms。

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.

注意:为了防止轮询阶段使事件循环陷入“饥饿”,libuv(C 语言实现的 Node.js 事件循环和所有平台异步行为的库)也有一个限制轮询次数的最大值(依赖于系统)。

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 套接字在尝试连接时接收到econnrejected,则某些 *nix 系统希望等待报告错误。这将在挂起回调阶段排队执行。

poll 轮询

The poll phase has two main functions:

轮询阶段有两个主要功能:

  1. Calculating how long it should block and poll for I/O, then
  2. Processing events in the poll queue.
  1. 计算需要阻塞多久然后轮询 I/O,然后
  2. 处理轮询队列中的事件。

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

当事件循环进入轮询阶段,并且没有计时器被调度时,将发生以下两种情况之一:

  • 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.
  • If the poll queue is empty, one of two more things will happen:
    • If scripts have been scheduled by setImmediate(), the event loop will end the poll phase and continue to the check phase to execute those scheduled scripts.
    • 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()调度,事件循环将结束轮询阶段,并且进入检查阶段去执行这些调度脚本。
    • 如果脚本没有setImmediate()调度,事件循环将等待回调被添加到队列中,然后立即执行。

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.

一旦轮询队列为空,事件循环将检查计时器的时间阈值是否已达到。如果一个或多个计时器就绪,则事件循环将回到计时器阶段,以执行这些计时器的回调。

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.

此阶段允许用户在轮询阶段完成后立即执行回调。如果轮询阶段变得空闲,并且脚本已经执行setImmediate()进入队列,则事件循环可能会继续到检查阶段而不是等待。

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()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv API,在轮询阶段完成后执行回调。

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 check phase rather than waiting for poll events.

一般来说,代码执行时,事件循环最终将进入轮询阶段,在此阶段它将等待传入的连接、请求等。但是,如果使用setImmediate()设置了一个回调,并且轮询阶段变得空闲,那么它将结束并进入到检查阶段,而不是等待轮询事件。

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().

如果一个套接字或处理函数突然关闭(例如socket.destroy()),“关闭”事件将在这个阶段发出。否则,它将通过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.
  • setTimeout() schedules a script to be run after a minimum threshold in ms has elapsed.
  • setImmediate()被设计成在当前轮询阶段完成后执行脚本。
  • 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).

执行计时器的顺序将根据调用它们的上下文而有所不同。如果它们都是在主模块被调用,则计时将受到进程性能的限制(可能受到机器上运行的其他应用程序的影响)。

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 循环中(即主模块),那么执行两个计时器的顺序是不确定的,因为它受到进程性能的限制:

// 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 周期内,立即回调总是先执行:

// 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 usingsetImmediate()oversetTimeout()issetImmediate()will always be executed before any timers if scheduled within an I/O cycle, independently of how many timers are present.

setTimeout()相比,使用setImmediate()的主要优势是setImmediate()总是在任何计时器之前执行(如果在 I/O 周期内调度的话),这与当前的计时器数量无关。

process.nextTick()

Understanding process.nextTick() 理解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 is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.

您可能已经注意到process.nextTick()没有显示在关系图中,尽管它是异步 API 的一部分。这是因为process.nextTick()严格来说不是事件循环的一部分。相反,nextTickQueue将在当前操作完成后处理,而不管事件循环的当前阶段。这里,操作被定义为底层 C/ C++ 处理程序转换,处理需要执行的JavaScript。

Looking back at our diagram, any time you callprocess.nextTick()in a given phase, all callbacks passed toprocess.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 recursiveprocess.nextTick()calls, which prevents the event loop from reaching the poll phase.

回顾我们的关系图,在给定阶段中每次调用process.nextTick()时,传递给process.nextTick()的所有回调都会在事件循环继续之前被解析。这可能会造成一些糟糕的情况,因为它允许通过递归调用process.nextTick()来“饿死” I/O,阻止事件循环到达轮询阶段。

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 中呢?一部分是因为这是一种设计哲学,在这种哲学中,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.

这段代码做参数检查,如果不正确,它将把错误传递给回调。该 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()总是在用户的其余代码之后和允许事件循环继续之前运行它的回调。为了实现这一点,JS 调用栈允许被展开(释放),然后立即执行提供的回调,这允许用户递归调用process.nextTick(),而不会触发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()的回调在事件循环的同一阶段被调用,因为someAsyncApiCall()实际上并不是异步的。结果,回调尝试引用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()中,脚本仍然能够运行到完成,允许在调用回调之前初始化所有变量、函数等。它还具有不允许事件循环继续的优势。在允许事件循环继续之前,发出错误警报,这对用户可能很有用。下面是前面使用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.

当只传递一个端口时,端口立即绑定。因此,'listening'回调会立即被调用。问题是.on('listening')回调在那个时候还没有被设置。

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.

为了解决这个问题,让'listening'事件在nextTick()中排队,以允许脚本运行完成。这允许用户设置任何他们想要的事件处理程序。

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
  • setImmediate() fires on the following iteration or 'tick' of the event loop
  • nexttick()在同一阶段立即触发
  • setImmediate()在事件循环的下一迭代或 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.

我们建议开发人员在所有情况下都使用setImmediate(),因为这样更容易理解。

Why use 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.
  2. At times it's necessary to allow a callback to run after the call stack has unwound but before the event loop continues.
  1. 允许用户处理错误,清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
  2. 有时有必要允许回调在调用栈释放之后继续事件循环之前运行。

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()在事件循环的开头运行,但是监听回调位于setImmediate()。除非传递了主机名,否则将立即绑定到端口。要使事件循环继续,它必须进入轮询阶段,这意味着有一定的概率接收到连接,从而允许在监听事件之前触发连接事件。

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 useprocess.nextTick()to set a callback to emit the event after the constructor has finished, which provides the expected results:

您不能立即从构造函数发出事件,因为脚本不会处理到用户为该事件分配回调。因此,在构造函数本身中,可以使用process. nexttick()设置回调,以便在构造函数完成后发出事件,这将提供预期的结果:

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!');
});

参考

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/https://zhuanlan.zhihu.com/p/33058983https://www.ibm.com/developerworks/cn/opensource/os-tutorials-learn-nodejs-the-event-loop/index.htmlhttps://zhuanlan.zhihu.com/p/59746093http://blog.yangfch3.com/2018/11/01/event-loop-timers-and-nexttick-cn/https://cloud.tencent.com/developer/article/1558961

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值