ノードの「イベント ループ」は、大規模な同時実行と高スループットを処理する機能の中核です。これは最も魔法の部分であり、これによれば、Node.js は基本的に「シングルスレッド」として理解され、同時にバックグラウンドでの任意の操作の処理も可能になります。この記事では、イベント ループがどのように機能するかを説明し、その魅力を感じていただけるようにします。
イベント駆動型プログラミング
イベント ループを理解するには、まずイベント駆動型プログラミングを理解する必要があります。 1960年に登場しました。現在、UI プログラミングではイベント駆動型プログラミングが頻繁に使用されています。 JavaScript の主な用途の 1 つは DOM と対話することなので、イベントベースの API を使用するのは自然なことです。
簡単に定義すると、イベント駆動型プログラミングは、イベントまたは状態の変化を通じてアプリケーションのフローを制御します。一般にイベント監視を通じて実装され、イベントが検出されると (つまり、状態が変化すると)、対応するコールバック関数が呼び出されます。おなじみですね?実際、これは Node.js イベント ループの基本的な動作原理です。
クライアント側の JavaScript 開発に精通している場合は、DOM 要素と組み合わせてユーザー インタラクションを提供するために使用される、element.onclick() などの .on*() メソッドについて考えてみましょう。この動作モードでは、単一のインスタンスで複数のイベントをトリガーできます。 Node.js は、サーバー側のソケットや「http」モジュールなどの EventEmitters (イベント ジェネレーター) を通じてこのパターンをトリガーします。 1 つ以上の状態変更を単一のインスタンスからトリガーできます。
もう 1 つの一般的なパターンは、成功と失敗を表現することです。一般に、2 つの一般的な実装方法があります。 1 つ目は、「エラー例外」を通常はコールバック関数の最初のパラメータとしてコールバックに渡すことです。 2 つ目は Promises デザイン パターンを使用し、ES6 を追加しています。注* Promise モードは、次のようなコールバック関数の深いネストを避けるために、jQuery と同様の関数チェーン記述方法を使用します。
よくある誤解は、イベント エミッターもイベントの起動時に本質的に非同期であるということですが、これは正しくありません。以下は、これを示す簡単なコード スニペットです。
console.log('before')
エミッター.emit('火')
console.log('after')}
};
me.on('火', function() {
console.log('emit fired');
});
// 出力:
// 前
// 発砲される
// 後
である必要があります
// 前
// 後
// 発砲される
EventEmitter は、非同期で完了する必要がある操作を通知するために使用されることが多いため、非同期的に動作することがよくありますが、EventEmitter API 自体は完全に同期しています。リスニング関数は内部的に非同期で実行できますが、すべてのリスニング関数は追加された順序で同期的に実行されることに注意してください。
メカニズムの概要とスレッドプール
ノード自体は複数のライブラリに依存しています。その 1 つは、非同期イベント キューと実行を処理するための素晴らしいライブラリである libuv です。
ノードは、既存の機能を実装するためにオペレーティング システム カーネルを可能な限り利用します。応答リクエストの生成、接続の転送、処理のためのシステムへの委託など。たとえば、着信接続は、Node で処理できるようになるまで、オペレーティング システムを通じてキューに入れられます。
Node にはスレッド プールがあると聞いたことがあるかもしれません。「Node がタスクを順番に処理するのに、なぜスレッド プールが必要なのでしょうか? これは、カーネル内ですべてのタスクが処理されるわけではないためです。」命令は非同期で実行されます。この場合、Node.JS は、ブロックされることなくイベント ループの実行を継続できるように、動作中にスレッドを一定期間ロックできる必要があります。
以下は、内部動作メカニズムを示す簡単な図の例です:
┌─────────────┐
╭─►│ タイマー タイマー
│ └───────┬───────┘
│ ┌───────┴───────┐
call係中のコールバック
│└。—七面
|
│ │ │ POLL ││── 接続、 │
│以来
│┌。—七面
╰─── ┤ setImmediate
━━━━━━━━━━┘
イベント ループの内部動作については、理解するのが難しい点がいくつかあります。
すべてのコールバックは、イベント ループ (タイマーなど) の 1 つのステージの終了時と次のステージに移行する前に、process.nextTick() を介してプリセットされます。これにより、無限ループを引き起こす process.nextTick() への再帰呼び出しの可能性が回避されます。
「保留中のコールバック」とは、他のイベント ループ サイクルによって処理されないコールバック キュー内のコールバック (たとえば、fs.write に渡される) です。
EventEmitter を作成することで、イベント ループとの対話を簡素化します。これは、イベントベースの API をより簡単に作成できるようにする汎用ラッパーです。この 2 つがどのように相互作用するかは、開発者を混乱させることがよくあります。
次の例は、イベントが同期的にトリガーされることを忘れると、イベントが見逃される可能性があることを示しています。
関数 MyThing() {
EventEmitter.call(this);
doFirstThing();
this.emit('thing1');
}
util.inherits(MyThing, EventEmitter);
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// 申し訳ありませんが、このイベントは決して発生しません
});
関数 MyThing() {
EventEmitter.call(this);
doFirstThing();
setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);
関数 EmitThing1(self) {
self.emit('thing1');
}
var mt = new MyThing();
mt.on('thing1', function onThing1() {
// 実行されました
});
次のソリューションも機能しますが、パフォーマンスが多少犠牲になります:
doFirstThing();
// Function#bind() を使用するとパフォーマンスが低下します
setImmediate(this.emit.bind(this, 'thing1'));
}
util.inherits(MyThing, EventEmitter);
// エラーをトリガーし、即座に (同期的に) 処理します
var er = doSecondThing();
if (er) {
This.emit('エラー', 'さらに悪いこと');
戻る;
}
}
結論
この記事では、イベント ループの内部動作と技術的な詳細について簡単に説明します。それはすべてよく考えられています。別の記事では、イベント ループとシステム カーネルの相互作用について説明し、NodeJS の非同期操作の魅力を示します。