Node의 이벤트 루프 및 process.nextTick() 이해
이 글은 Nodejs의 이벤트 루프에 대한 이해를 돕고, 이벤트 루프 메커니즘, process.nextTick() 등을 분석합니다. 모두에게 도움이 되기를 바랍니다!
이벤트 루프란 무엇입니까
이벤트 루프는 Node.js가 비차단 I/O 작업을 처리하는 메커니즘입니다. JavaScript가 단일 스레드임에도 불구하고 가능한 경우 시스템에 작업을 오프로드합니다. 커널.
이제 대부분의 코어는 멀티스레드이므로 백그라운드에서 여러 작업을 처리할 수 있습니다. 작업 중 하나가 완료되면 커널은 Node.js에 적절한 콜백 함수를 폴링 대기열에 추가하고 실행을 기다리도록 알립니다. 이 글의 뒷부분에서 자세히 소개하겠습니다.
이벤트 루프 메커니즘 분석
Node.js가 시작되면 이벤트 루프를 초기화하고 제공된 입력 스크립트를 처리하며(또는 이 문서에서 다루지 않는 REPL에 넣습니다) 호출할 수 있습니다. 일부 비동기 API를 사용하거나 타이머를 예약하거나 process.nextTick()
을 호출하고 이벤트 루프 처리를 시작하세요. process.nextTick()
,然后开始处理事件循环。
下面的图表展示了事件循环操作顺序的简化概览。
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
注意:每个框被称为事件循环机制的一个阶段。
每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。
由于这些操作中的任何一个都可能调度_更多的_操作和由内核排列在轮询阶段被处理的新事件, 且在处理轮询中的事件时,轮询事件可以排队。因此,长时间运行的回调可以允许轮询阶段运行长于计时器的阈值时间。有关详细信息,请参阅 计时器 和 轮询 部分。
注意: 在 Windows 和 Unix/Linux 实现之间存在细微的差异,但这对演示来说并不重要。最重要的部分在这里。实际上有七或八个步骤,但我们关心的是 Node.js 实际上使用以上的某些步骤。
阶段概述
- 定时器:本阶段执行已经被
setTimeout()
和setInterval()
的调度回调函数。 - 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()
调度的之外),其余情况 node 将在适当的时候在此阻塞。 - 检测:
setImmediate()
回调函数在这里执行。 - 关闭的回调函数:一些关闭的回调函数,如:
socket.on('close', ...)
아래 다이어그램은 이벤트 루프의 작업 순서에 대한 간략한 개요를 보여줍니다.
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 } });
각 단계에는 콜백을 실행하기 위한 FIFO 대기열이 있습니다. 각 단계는 특별하지만 일반적으로 이벤트 루프가 특정 단계에 들어갈 때 해당 단계에 특정한 작업을 수행한 다음 대기열이 소진되거나 최대 콜백 수가 실행될 때까지 해당 단계의 대기열에서 콜백을 실행합니다. 대기열이 소진되거나 콜백 제한에 도달하면 이벤트 루프가 다음 단계로 이동합니다. 이러한 작업 중 하나라도
폴링단계에서 처리되도록 커널에 의해 대기열에 추가된 작업과 새 이벤트를 예약할 수 있고 폴링 단계에서 이벤트를 처리하는 동안 폴링 이벤트가 대기열에 포함될 수 있습니다. 따라서 장기 실행 콜백을 사용하면 폴링 단계가 타이머의 임계값 시간보다 오래 실행될 수 있습니다. 자세한 내용은 Timer
및Polling 부분. 참고: Windows와 Unix/Linux 구현 간에는 미묘한 차이가 있지만 이는 데모에서는 중요하지 않습니다. 가장 중요한 부분은 여기에 있습니다. 실제로는 7~8개의 단계가 있지만 우리가 관심을 갖는 것은 Node.js가 실제로 위의 단계 중 일부를 사용한다는 것입니다.
단계 개요- 🎜Timer🎜: 이 단계의 실행은
setTimeout()
및 에 의해 제어되었습니다. setInterval()
의 스케줄링 콜백 함수. 🎜 - 🎜보류 중인 콜백🎜: 다음 루프 반복까지 실행이 지연되는 I/O 콜백입니다. 🎜
- 🎜idle, prepare🎜: 시스템 내부에서만 사용됩니다. 🎜
- 🎜Polling🎜: 새 I/O 이벤트 검색, I/O 관련 콜백 실행(거의 모든 경우, 타이머 및
setImmediate()</ code>에 의해 처리되는 종료 콜백 제외) 다른 경우 노드는 적절한 시간에 여기에서 차단됩니다. 🎜<li>🎜Detection🎜: <code>setImmediate()
여기서 콜백 함수가 실행됩니다. 🎜 - 🎜닫힌 콜백 함수🎜:
socket.on('close', ...)
와 같은 일부 닫힌 콜백 함수. 🎜🎜🎜각 이벤트 루프 실행 사이에 Node.js는 비동기 I/O 또는 타이머를 기다리고 있는지 확인하고 그렇지 않은 경우 완전히 종료됩니다. 🎜🎜단계에 대한 자세한 개요 🎜🎜🎜타이머 🎜🎜🎜타이머는 사용자가 실행하기를 원하는 정확한 시간이 아니라 제공된 콜백이 실행될 수 있는 🎜임계값🎜을 지정합니다. 지정된 간격이 지나면 타이머 콜백이 최대한 빨리 실행됩니다. 그러나 운영 체제 예약이나 기타 실행 중인 콜백으로 인해 지연될 수 있습니다. 🎜🎜🎜🎜Note🎜: 🎜🎜polling🎜 단계🎜는 타이머가 실행되는 시기를 제어합니다. 🎜🎜🎜예를 들어, 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
}
});
로그인 후 복사로그인 후 복사当事件循环进入 轮询 阶段时,它有一个空队列(此时 fs.readFile()
尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它等待 95 毫秒过后时,fs.readFile()
完成读取文件,它的那个需要 10 毫秒才能完成的回调,将被添加到 轮询 队列中并执行。当回调完成时,队列中不再有回调,因此事件循环机制将查看最快到达阈值的计时器,然后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。
注意:为了防止 轮询 阶段饿死事件循环,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个硬性最大值(依赖于系统)。
挂起的回调函数
此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED
,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。
轮询
轮询 阶段有两个重要的功能:
计算应该阻塞和轮询 I/O 的时间。
然后,处理 轮询 队列里的事件。
当事件循环进入 轮询 阶段且_没有被调度的计时器时_,将发生以下两种情况之一:
如果 轮询 队列 不是空的
,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
如果 轮询 队列 是空的,还有两件事发生:
如果脚本被 setImmediate()
调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。
如果脚本 未被 setImmediate()
调度,则事件循环将等待回调被添加到队列中,然后立即执行。
一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
检查阶段
此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate()
后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。
setImmediate()
实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()
调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。
关闭的回调函数
如果套接字或处理函数突然关闭(例如 socket.destroy()
),则'close'
事件将在这个阶段发出。否则它将通过 process.nextTick()
发出。
setImmediate() 对比 setTimeout()
setImmediate()
和 setTimeout()
很类似,但是基于被调用的时机,他们也有不同表现。
setImmediate()
设计为一旦在当前 轮询 阶段完成, 就执行脚本。setTimeout()
在最小阈值(ms 单位)过后运行脚本。
执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
例如,如果运行以下不在 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
로그인 후 복사但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:
// 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
로그인 후 복사使用 setImmediate()
相对于setTimeout()
的主要优势是,如果setImmediate()
是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关
process.nextTick()
理解 process.nextTick()
您可能已经注意到 process.nextTick()
在图示中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick()
从技术上讲不是事件循环的一部分。相反,它都将在当前操作完成后处理 nextTickQueue
, 而不管事件循环的当前阶段如何。这里的一个_操作_被视作为一个从底层 C/C++ 处理器开始过渡,并且处理需要执行的 JavaScript 代码。
回顾我们的图示,任何时候在给定的阶段中调用 process.nextTick()
,所有传递到 process.nextTick()
的回调将在事件循环继续之前解析。这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()
调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。
为什么会允许这样?
为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(
callback,
new TypeError('argument should be string')
);
}
로그인 후 복사代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 API 进行了更新,允许传递参数给 process.nextTick()
,这将允许它接受任何在回调函数位置之后的参数,并将参数传递给回调函数作为回调函数的参数,这样您就不必嵌套函数了。
我们正在做的是将错误传回给用户,但仅在执行用户的其余代码之后。通过使用process.nextTick()
,我们保证 apiCall()
始终在用户代码的其余部分_之后_和在让事件循环继续进行_之前_,执行其回调函数。为了实现这一点,JS 调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用 process.nextTick()
,而不触碰 RangeError: 超过 V8 的最大调用堆栈大小
限制。
这种设计原理可能会导致一些潜在的问题。 以此代码段为例:
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;
로그인 후 복사用户将 someAsyncApiCall()
定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给 someAsyncApiCall()
的回调是在事件循环的同一阶段内被调用,因为 someAsyncApiCall()
实际上并没有异步执行任何事情。结果,回调函数在尝试引用 bar
,但作用域中可能还没有该变量,因为脚本尚未运行完成。
通过将回调置于 process.nextTick()
中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有的变量、函数等。它还具有不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况。下面是上一个使用 process.nextTick()
的示例:
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
로그인 후 복사这又是另外一个真实的例子:
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
로그인 후 복사只有传递端口时,端口才会立即被绑定。因此,可以立即调用 'listening'
回调。问题是 .on('listening')
的回调在那个时间点尚未被设置。
为了绕过这个问题,'listening'
事件被排在 nextTick()
中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。
process.nextTick() 对比 setImmediate()
就用户而言,我们有两个类似的调用,但它们的名称令人费解。
process.nextTick()
在同一个阶段立即执行。setImmediate()
在事件循环的接下来的迭代或 'tick' 上触发。
实质上,这两个名称应该交换,因为 process.nextTick()
比 setImmediate()
触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。
我们建议开发人员在所有情况下都使用 setImmediate()
,因为它更容易理解。
为什么要使用 process.nextTick()?
有两个主要原因:
允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
以下是一个符合用户预期的简单示例:
const server = net.createServer();
server.on('connection', (conn) => {});
server.listen(8080);
server.on('listening', () => {});
로그인 후 복사假设 listen()
在事件循环开始时运行,但 listening 的回调被放置在 setImmediate()
中。除非传递过主机名,才会立即绑定到端口。为使事件循环继续进行,它必须命中 轮询 阶段,这意味着有可能已经接收了一个连接,并在侦听事件之前触发了连接事件。
另一个示例运行的函数构造函数是从 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!');
});
로그인 후 복사你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 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/
更多node相关知识,请访问:nodejs 教程!
위 내용은 Node의 이벤트 루프 및 process.nextTick() 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!
setTimeout()
및 에 의해 제어되었습니다. setInterval()
의 스케줄링 콜백 함수. 🎜setImmediate()</ code>에 의해 처리되는 종료 콜백 제외) 다른 경우 노드는 적절한 시간에 여기에서 차단됩니다. 🎜<li>🎜Detection🎜: <code>setImmediate()
여기서 콜백 함수가 실행됩니다. 🎜socket.on('close', ...)
와 같은 일부 닫힌 콜백 함수. 🎜🎜🎜각 이벤트 루프 실행 사이에 Node.js는 비동기 I/O 또는 타이머를 기다리고 있는지 확인하고 그렇지 않은 경우 완전히 종료됩니다. 🎜🎜단계에 대한 자세한 개요 🎜🎜🎜타이머 🎜🎜🎜타이머는 사용자가 실행하기를 원하는 정확한 시간이 아니라 제공된 콜백이 실행될 수 있는 🎜임계값🎜을 지정합니다. 지정된 간격이 지나면 타이머 콜백이 최대한 빨리 실행됩니다. 그러나 운영 체제 예약이나 기타 실행 중인 콜백으로 인해 지연될 수 있습니다. 🎜🎜🎜🎜Note🎜: 🎜🎜polling🎜 단계🎜는 타이머가 실행되는 시기를 제어합니다. 🎜🎜🎜예를 들어, 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 } });
当事件循环进入 轮询 阶段时,它有一个空队列(此时 fs.readFile()
尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它等待 95 毫秒过后时,fs.readFile()
完成读取文件,它的那个需要 10 毫秒才能完成的回调,将被添加到 轮询 队列中并执行。当回调完成时,队列中不再有回调,因此事件循环机制将查看最快到达阈值的计时器,然后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。
注意:为了防止 轮询 阶段饿死事件循环,libuv(实现 Node.js 事件循环和平台的所有异步行为的 C 函数库),在停止轮询以获得更多事件之前,还有一个硬性最大值(依赖于系统)。
挂起的回调函数
此阶段对某些系统操作(如 TCP 错误类型)执行回调。例如,如果 TCP 套接字在尝试连接时接收到 ECONNREFUSED
,则某些 *nix 的系统希望等待报告错误。这将被排队以在 挂起的回调 阶段执行。
轮询
轮询 阶段有两个重要的功能:
计算应该阻塞和轮询 I/O 的时间。
然后,处理 轮询 队列里的事件。
当事件循环进入 轮询 阶段且_没有被调度的计时器时_,将发生以下两种情况之一:
如果 轮询 队列 不是空的
,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
如果 轮询 队列 是空的,还有两件事发生:
如果脚本被
setImmediate()
调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。如果脚本 未被
setImmediate()
调度,则事件循环将等待回调被添加到队列中,然后立即执行。
一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。
检查阶段
此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate()
后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。
setImmediate()
实际上是一个在事件循环的单独阶段运行的特殊计时器。它使用一个 libuv API 来安排回调在 轮询 阶段完成后执行。
通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()
调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。
关闭的回调函数
如果套接字或处理函数突然关闭(例如 socket.destroy()
),则'close'
事件将在这个阶段发出。否则它将通过 process.nextTick()
发出。
setImmediate() 对比 setTimeout()
setImmediate()
和 setTimeout()
很类似,但是基于被调用的时机,他们也有不同表现。
setImmediate()
设计为一旦在当前 轮询 阶段完成, 就执行脚本。setTimeout()
在最小阈值(ms 单位)过后运行脚本。
执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。
例如,如果运行以下不在 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
但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:
// 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
使用 setImmediate()
相对于setTimeout()
的主要优势是,如果setImmediate()
是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关
process.nextTick()
理解 process.nextTick()
您可能已经注意到 process.nextTick()
在图示中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick()
从技术上讲不是事件循环的一部分。相反,它都将在当前操作完成后处理 nextTickQueue
, 而不管事件循环的当前阶段如何。这里的一个_操作_被视作为一个从底层 C/C++ 处理器开始过渡,并且处理需要执行的 JavaScript 代码。
回顾我们的图示,任何时候在给定的阶段中调用 process.nextTick()
,所有传递到 process.nextTick()
的回调将在事件循环继续之前解析。这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()
调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。
为什么会允许这样?
为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick( callback, new TypeError('argument should be string') ); }
代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 API 进行了更新,允许传递参数给 process.nextTick()
,这将允许它接受任何在回调函数位置之后的参数,并将参数传递给回调函数作为回调函数的参数,这样您就不必嵌套函数了。
我们正在做的是将错误传回给用户,但仅在执行用户的其余代码之后。通过使用process.nextTick()
,我们保证 apiCall()
始终在用户代码的其余部分_之后_和在让事件循环继续进行_之前_,执行其回调函数。为了实现这一点,JS 调用栈被允许展开,然后立即执行提供的回调,允许进行递归调用 process.nextTick()
,而不触碰 RangeError: 超过 V8 的最大调用堆栈大小
限制。
这种设计原理可能会导致一些潜在的问题。 以此代码段为例:
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;
用户将 someAsyncApiCall()
定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给 someAsyncApiCall()
的回调是在事件循环的同一阶段内被调用,因为 someAsyncApiCall()
实际上并没有异步执行任何事情。结果,回调函数在尝试引用 bar
,但作用域中可能还没有该变量,因为脚本尚未运行完成。
通过将回调置于 process.nextTick()
中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有的变量、函数等。它还具有不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况。下面是上一个使用 process.nextTick()
的示例:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1;
这又是另外一个真实的例子:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
只有传递端口时,端口才会立即被绑定。因此,可以立即调用 'listening'
回调。问题是 .on('listening')
的回调在那个时间点尚未被设置。
为了绕过这个问题,'listening'
事件被排在 nextTick()
中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。
process.nextTick() 对比 setImmediate()
就用户而言,我们有两个类似的调用,但它们的名称令人费解。
process.nextTick()
在同一个阶段立即执行。setImmediate()
在事件循环的接下来的迭代或 'tick' 上触发。
实质上,这两个名称应该交换,因为 process.nextTick()
比 setImmediate()
触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。
我们建议开发人员在所有情况下都使用 setImmediate()
,因为它更容易理解。
为什么要使用 process.nextTick()?
有两个主要原因:
允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
以下是一个符合用户预期的简单示例:
const server = net.createServer(); server.on('connection', (conn) => {}); server.listen(8080); server.on('listening', () => {});
假设 listen()
在事件循环开始时运行,但 listening 的回调被放置在 setImmediate()
中。除非传递过主机名,才会立即绑定到端口。为使事件循环继续进行,它必须命中 轮询 阶段,这意味着有可能已经接收了一个连接,并在侦听事件之前触发了连接事件。
另一个示例运行的函数构造函数是从 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!'); });
你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 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/
更多node相关知识,请访问:nodejs 教程!
위 내용은 Node의 이벤트 루프 및 process.nextTick() 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











이 기사는 NodeJS V8 엔진의 메모리 및 가비지 수집기(GC)에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다.

Non-Blocking, Event-Driven 기반으로 구축된 Node 서비스는 메모리 소모가 적다는 장점이 있으며, 대규모 네트워크 요청을 처리하는데 매우 적합합니다. 대규모 요청을 전제로 '메모리 제어'와 관련된 문제를 고려해야 합니다. 1. V8의 가비지 수집 메커니즘과 메모리 제한 Js는 가비지 수집 기계에 의해 제어됩니다.

파일 모듈은 파일 읽기/쓰기/열기/닫기/삭제 추가 등과 같은 기본 파일 작업을 캡슐화한 것입니다. 파일 모듈의 가장 큰 특징은 모든 메소드가 **동기** 및 ** 두 가지 버전을 제공한다는 것입니다. 비동기**, sync 접미사가 있는 메서드는 모두 동기화 메서드이고, 없는 메서드는 모두 이기종 메서드입니다.

이벤트 루프는 Node.js의 기본 부분이며 메인 스레드가 차단되지 않도록 하여 비동기 프로그래밍을 가능하게 합니다. 이벤트 루프를 이해하는 것은 효율적인 애플리케이션을 구축하는 데 중요합니다. 다음 기사는 Node.js의 이벤트 루프에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다!

iPhone 카메라에서 타이머를 얼마나 오래 설정할 수 있나요? iPhone 카메라 앱의 타이머 옵션에 액세스하면 3초(3초)와 10초(10초)의 두 가지 모드 중에서 선택할 수 있는 옵션이 제공됩니다. 첫 번째 옵션을 사용하면 iPhone을 들고 전면 또는 후면 카메라로 빠르게 셀카를 찍을 수 있습니다. 두 번째 옵션은 멀리서 아이폰을 삼각대에 장착해 단체 사진이나 셀카를 찍을 수 있는 장면에서 유용하다. iPhone 카메라에서 타이머를 설정하는 방법 iPhone 카메라에서 타이머를 설정하는 것은 매우 간단한 과정이지만, 정확한 수행 방법은 사용 중인 iPhone 모델에 따라 다릅니다.

초기에 JS는 브라우저 측에서만 실행되었습니다. 유니코드로 인코딩된 문자열은 처리하기 쉬웠지만 바이너리 및 유니코드가 아닌 인코딩된 문자열을 처리하는 것은 어려웠습니다. 그리고 바이너리는 컴퓨터의 가장 낮은 데이터 형식인 비디오/오디오/프로그램/네트워크 패키지입니다.

노드가 npm 명령을 사용할 수 없는 이유는 환경 변수가 올바르게 구성되지 않았기 때문입니다. 해결 방법은 다음과 같습니다. 1. "시스템 속성"을 엽니다. 2. "환경 변수" -> "시스템 변수"를 찾은 다음 환경을 편집합니다. 3. nodejs 폴더의 위치를 찾습니다. 4. "확인"을 클릭합니다.

Workerman 문서에서 타이머 기능을 구현하는 방법 Workerman은 타이머 기능을 포함한 다양한 기능을 제공하는 강력한 PHP 비동기 네트워크 통신 프레임워크입니다. 타이머를 사용하여 지정된 시간 간격 내에서 코드를 실행합니다. 이는 예약된 작업 및 폴링과 같은 애플리케이션 시나리오에 매우 적합합니다. 다음으로는 Workerman에서 타이머 기능을 구현하는 방법을 자세히 소개하고 구체적인 코드 예시를 제공하겠습니다. 1단계: Workerman 설치 먼저 Worker를 설치해야 합니다.
