ホームページ > 記事 > ウェブフロントエンド > Nodeのイベントループとprocess.nextTick()を理解する
この記事では、Nodejsのイベントループを理解し、イベントループの仕組みやprocess.nextTick()などを分析していきます。皆様のお役に立てれば幸いです。
イベント ループは、JavaScript がシングルスレッド - - 可能であれば、操作をシステム カーネルに移動します。
現在のカーネルのほとんどはマルチスレッドであるため、バックグラウンドでさまざまな操作を処理できます。いずれかの操作が完了すると、カーネルは Node.js に適切なコールバック関数をポーリング キューに追加して実行を待つように通知します。この記事の後半で詳しく紹介します。
Node.js が開始されると、イベント ループが初期化され、提供された入力スクリプトが処理されます (または、それを REPL にスローされます)。この記事では説明していませんが)、いくつかの非同期 API を呼び出したり、タイマーをスケジュールしたり、process.nextTick()
を呼び出したりして、イベント ループの処理を開始することがあります。
下の図は、イベント ループの一連の操作の概要を簡略化して示しています。
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
注: 各ボックスは、イベント ループ メカニズムのステージと呼ばれます。
各ステージには、コールバックを実行するための FIFO キューがあります。各ステージは特殊ですが、通常、イベント ループが特定のステージに入ると、そのステージに固有の操作が実行され、キューが使い果たされるか最大数のコールバックが実行されるまで、そのステージのキュー内のコールバックが実行されます。キューが使い果たされるか、コールバック制限に達すると、イベント ループは次のフェーズに移行します。
これらの操作のいずれかが、 polling フェーズ中に処理されるようにカーネルによってキューに追加される _more_ 操作と新しいイベントをスケジュールする可能性があるため、ポーリング内のイベントの処理中に、ポーリング イベントをキューに入れることができます。 。したがって、コールバックの実行時間が長いと、ポーリング フェーズがタイマーのしきい値時間を超えて実行される可能性があります。詳細については、「タイマー」および「ポーリング」セクションを参照してください。
注: Windows 実装と Unix/Linux 実装の間には微妙な違いがありますが、これはデモの目的では重要ではありません。最も重要な部分はここです。実際には 7 つまたは 8 つのステップがありますが、ここで重要なのは、Node.js が実際に上記のステップの一部を使用しているということです。
setTimeout()
および setInterval( )
コールバック関数のスケジュール。 setImmediate() によって処理されます
スケジューリングを除く)、その他の場合、ノードは適切なタイミングでここでブロックされます。 setImmediate()
ここでコールバック関数が実行されます。 socket.on('close', ...)
などのいくつかのクローズド コールバック関数。 イベント ループの各実行の間に、Node.js は非同期 I/O またはタイマーを待機しているかどうかを確認し、待機していない場合は完全にシャットダウンします。
タイマーは、指定されたコールバックの代わりに実行できる しきい値 を指定します。 user 実行の正確な時間を予想します。指定された間隔が経過すると、タイマー コールバックができるだけ早く実行されます。ただし、オペレーティング システムのスケジューリングや他の実行中のコールバックによって遅延する可能性があります。
注: ポーリング フェーズは、タイマーの実行時期を制御します。
たとえば、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()
在最小阈值(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()
在图示中没有显示,即使它是异步 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()
在事件循环的接下来的迭代或 'tick' 上触发。实质上,这两个名称应该交换,因为 process.nextTick()
比 setImmediate()
触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。
我们建议开发人员在所有情况下都使用 setImmediate()
,因为它更容易理解。
有两个主要原因:
允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
以下是一个符合用户预期的简单示例:
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 中国語 Web サイトの他の関連記事を参照してください。