HTTPリクエスト、レスポンス、ストリームなどのほとんどのNodeオブジェクトは、 EventEmitter
モジュールを実装します。これにより、イベントを生成およびリッスンできます。
const EventEmitter = require('events')
イベント管理の最も単純な形式は、 fs.readFile
などの一般的なNode.js関数のコールバックスタイルです。 この類推により、イベントは1回生成され(Nodeがコールバックを呼び出す準備ができたとき)、コールバックはイベントハンドラーとして機能します。 最初に、イベント駆動型アーキテクチャのこの基本的な形式を見てみましょう。
準備ができたら電話してください、ノード!
ノードは最初にコールバックを使用して非同期イベントを処理しました。 これはかなり前のことで、Promiseとasync / await機能のネイティブサポートがJavaScriptに登場しました。 コールバックは、他の関数に渡す単なる関数です。 関数はファーストクラスのオブジェクトであるため、これはJavaScriptで可能です。
コールバックはコード内の非同期呼び出しの指標ではないことを理解することが重要です。 関数は、コールバックを同期的および非同期的に呼び出すことができます。 たとえば、 fileSize
ホスト関数はcb
コールバック関数を受け入れ、条件に応じて同期的または非同期的に呼び出します。
function fileSize (fileName, cb) { if (typeof fileName !== 'string') { return cb(new TypeError('argument should be string')); // Sync } fs.stat(fileName, (err, stats) => { if (err) { return cb(err); } // Async cb(null, stats.size); // Async }); }
これは悪いアプローチであり、予期しないエラーにつながります。 常に同期的または常に非同期的にコールバックを受け入れるホスト関数を作成します。
コールバックスタイルで記述された典型的な非同期Node関数の簡単な例を見てみましょう。
const readFileAsArray = function(file, cb) { fs.readFile(file, function(err, data) { if (err) { return cb(err); } const lines = data.toString().trim().split('\n'); cb(null, lines); }); };
readFileAsArray
は、ファイルパスとコールバック関数を取ります。 ファイルの内容を読み取り、文字列の配列に分割し、この配列のコールバック関数を呼び出します。 使用方法は次のとおりです。 numbers.txt
ファイルがこのコンテンツと同じディレクトリにあるとします。
10 11 12 13 14 15
このファイル内の数値をカウントするタスクがある場合、コードを簡素化するためにreadFileAsArray
を使用できます。
readFileAsArray('./numbers.txt', (err, lines) => { if (err) throw err; const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log('Odd numbers count:', oddNumbers.length); });
このコードは、文字列の配列内の数値コンテンツを読み取り、数値として解析し、カウントを実行します。
ここではノードのコールバックスタイルが機能します。 コールバックにはerr
error-first引数がありますが、これはnullの場合があります。 このコールバックをホスト関数の最後の引数として渡します。 ユーザーはほとんどの場合それを信頼するため、関数では常にそうします。 ホスト関数が最後の引数としてコールバックを受け取り、コールバックが最初の引数としてエラーオブジェクトを受け取るようにします。
コールバックに代わる最新のJS
最新のJavaScriptにはpromiseなどのオブジェクトがあります。 非同期APIの場合、コールバックの代わりに使用できます。 コールバックを引数として渡して同じ場所でエラーを処理する代わりに、promiseは成功とエラーの状況を別々に処理し、いくつかの非同期呼び出しをネストするのではなく、チェーンに接続できます。
readFileAsArray
関数がプロミスをサポートする場合、次のように使用できます。
readFileAsArray('./numbers.txt') .then(lines => { const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log('Odd numbers count:', oddNumbers.length); }) .catch(console.error);
コールバックを渡す代わりに、ホスト関数の戻り値に関連して.then
関数を呼び出します。 通常、 .then
は、コールバックバージョンで取得するのと同じ配列行にアクセスできるため、以前と同じように作業できます。 エラーを処理するには、結果に.catch
呼び出しを追加します。これにより、エラーが発生した場合にエラーにアクセスできます。
最新のJavaScriptの新しいPromiseオブジェクトのおかげで、ホスト関数を使用したPromiseインターフェースサポートの実装が容易になりました。 既にサポートされているコールバックインターフェイスに加えて、Promiseインターフェイスをサポートするように変更されたreadFileAsArray
関数は次のreadFileAsArray
です。
const readFileAsArray = function(file, cb = () => {}) { return new Promise((resolve, reject) => { fs.readFile(file, function(err, data) { if (err) { reject(err); return cb(err); } const lines = data.toString().trim().split('\n'); resolve(lines); cb(null, lines); }); }); };
この関数は、非同期fs.readFile
呼び出しがfs.readFile
たPromiseオブジェクトを返します。 promiseには、 resolve
関数とreject
関数の2つの引数があります。 エラーでコールバックを呼び出す必要がある場合は、プロミスをreject
関数を使用し、データを含むコールバックではプロミスをresolve
する関数を使用します。
唯一の違いは、promiseインターフェイスでコードが使用される場合に備えて、コールバック引数のデフォルト値が必要なことです。 たとえば、単純なデフォルトの空の関数() => {}
を引数として使用できます。
async / awaitを使用してPromiseを適用する
promiseインターフェースを追加すると、ループ内で非同期関数を使用する必要がある場合にコードを操作しやすくなります。 コールバックでは、状況はより複雑です。 関数ジェネレーターと同様に、約束は物事を少し改善します。 つまり、非同期コードを操作するためのより最近の代替手段は、 async
関数です。 非同期コードを同期として扱うことができるため、コードの可読性が大幅に向上します。
readFileAsArray
関数をasync / awaitで使用する方法はreadFileAsArray
です。
async function countOdd () { try { const lines = await readFileAsArray('./numbers'); const numbers = lines.map(Number); const oddCount = numbers.filter(n => n%2 === 1).length; console.log('Odd numbers count:', oddCount); } catch(err) { console.error(err); } } countOdd();
最初に、非同期関数を作成します-最初にasync
という単語を含む通常の関数です。 その中で、 readFileAsArray
関数を呼び出して、行変数を返すかのように、このためにawait
キーワードを使用します。 readFileAsArray
呼び出しが同期であった場合、コードを続行します。 これを達成するために、非同期関数を実行します。 だから、シンプルで読みやすいことがわかりました。 エラーに対処するには、非同期呼び出しをtry/catch
式でラップする必要があります。
非同期/待機機能のおかげで、特別なAPI(.thenや.catchなど)は必要ありませんでした。 関数に異なるラベルを付けて、純粋なJavaScriptを使用しました。
promiseインターフェースをサポートする任意の関数でasync / awaitを使用できます。 ただし、非同期コールバックスタイルの関数(setTimeoutなど)を使用することはできません。
EventEmitterモジュール
EventEmitterは、Node内のオブジェクト間の通信を容易にするモジュールです。 これは、非同期イベント駆動型アーキテクチャの中核です。 Nodeに組み込まれているモジュールの多くは、EventEmitterを継承しています。
彼のアイデアはシンプルです。エミッターオブジェクトは、以前に登録されたリスナーを呼び出す名前付きイベントを生成します。 そのため、エミッタには2つの主な機能があります。
- 名前付きイベントの生成。
- リスナー関数の登録と登録解除。
EventEmitterを使用するには、拡張機能クラスを作成する必要があります。
class MyEmitter extends EventEmitter { }
エミッターは、EventEmitterに基づくクラスからインスタンス化するものです。
const myEmitter = new MyEmitter();
エミッターのライフサイクルのどの時点でも、emit関数を使用して名前付きイベントを生成できます。
myEmitter.emit('something-happened');
イベント生成は、何らかの条件が満たされたことを示すシグナルです。 通常、生成オブジェクトの状態を変更することについて話します。 on
メソッドを使用するon
、エミッターが関連付けられた名前付きイベントを生成するたびに実行されるリスナー関数を追加できます。
イベント!==非同期
例を見てみましょう:
const EventEmitter = require('events'); class WithLog extends EventEmitter { execute(taskFunc) { console.log('Before executing'); this.emit('begin'); taskFunc(); this.emit('end'); console.log('After executing'); } } const withLog = new WithLog(); withLog.on('begin', () => console.log('About to execute')); withLog.on('end', () => console.log('Done with execute')); withLog.execute(() => console.log('*** Executing task ***'));
WithLog
クラスはエミッターです。 execute
関数の1つのインスタンスを定義します。 タスク関数という1つの引数を受け取り、その実行をログ式でラップします。 イベントは実行の前後に生成されます。
すべてが機能する順序を確認するには、名前付きイベントのリスナーを登録し、サンプルタスクを実行してチェーン全体を実行します。
結果:
Before executing About to execute *** Executing task *** Done with execute After executing
コード実行の結果に関して注意したいこと:非同期はありません。
- まず、「実行前」という行を取得します。
- 次に、名前付きの
begin
イベントの結果、文字列「About to execute」が生成されます。 - 次に、実際に実行可能な行は、
«*** Executing task ***»
という行を生成し«*** Executing task ***»
。 - 次に、名前付き
end
イベントの結果、文字列「Done with execute」が生成されます。 - 最後に、「実行後」という行が表示されます。
古き良きコールバックと同様に、イベントが同期コードまたは非同期コードの特性であることを示唆していません。 execute
に非同期taskFuncを渡すと、生成されたイベントが正確でなくなるため、これは重要です。
setImmediate
を呼び出すことにより、この状況をエミュレートできます。
// ... withLog.execute(() => { setImmediate(() => { console.log('*** Executing task ***') }); });
結果は次のようになります。
Before executing About to execute Done with execute After executing *** Executing task ***
これは間違っています。 非同期呼び出しの後の行は、「実行済み」および「実行後」という呼び出しの出現につながり、間違った順序で表示されます。
非同期関数の完了後にイベントを生成するには、コールバック(またはプロミス)をこのイベント駆動型の通信と組み合わせる必要があります。 これを以下の例で示します。
通常のコールバックの代わりにイベントを使用する利点の1つは、複数のリスナーの定義のおかげで、同じ信号に何度も応答できることです。 コールバックで同じことを行うには、利用可能な1つのコールバック内により多くのロジックを記述する必要があります。 イベントは、アプリケーションのコアに機能を追加する多数の外部プラグインを実装する素晴らしい方法です。 状態を変更するときの動作をカスタマイズするための「コネクタ」と見なすことができます。
非同期イベント
同期の例を、非同期でもう少し便利なものに変換しましょう。
const fs = require('fs'); const EventEmitter = require('events'); class WithTime extends EventEmitter { execute(asyncFunc, ...args) { this.emit('begin'); console.time('execute'); asyncFunc(...args, (err, data) => { if (err) { return this.emit('error', err); } this.emit('data', data); console.timeEnd('execute'); this.emit('end'); }); } } const withTime = new WithTime(); withTime.on('begin', () => console.log('About to execute')); withTime.on('end', () => console.log('Done with execute')); withTime.execute(fs.readFile, __filename);
WithTime
クラスはasyncFunc
を実行し、 console.time
およびconsole.timeEnd
呼び出しを使用して、このasyncFunc
費やした時間を報告します。 実行前後にイベントの正しいシーケンスを生成します。 また、通常の非同期呼び出し信号で動作するエラー/データイベントを生成します。
非同期関数fs.readFile
呼び出しを渡すことによりwithTime
エミッタwithTime
ます。 コールバックを使用してファイルのデータを処理する代わりに、データイベントをリッスンできるようになりました。
このコードを実行すると、予想どおり、正しいイベントシーケンスと実行時間に関するレポートが取得されます。
About to execute execute: 4.507ms Done with execute
このため、コールバックとエミッターを組み合わせる必要があることに注意してください。 asynFunc
もサポートしている場合、async / awaitを使用して同じすべてを実装できます。
class WithTime extends EventEmitter { async execute(asyncFunc, ...args) { this.emit('begin'); try { console.time('execute'); const data = await asyncFunc(...args); this.emit('data', data); console.timeEnd('execute'); this.emit('end'); } catch(err) { this.emit('error', err); } } }
あなたにとってどのようにしたらよいかわかりませんが、私にとっては、コールバックまたは.then / .catchを使用した行に基づくコードよりもはるかに読みやすくなっています。 async / await機能により、できる限りJavaScriptに近づけることができ、これは素晴らしい成果だと思います。
イベントとエラーの引数
前の例では、追加の引数を使用して2つのイベントが生成されました。 エラーイベントは、エラーオブジェクトによって生成されました。
this.emit('error', err);
データイベントは、データオブジェクトによって生成されます。
this.emit('data', data);
名前付きイベントの後、必要な数の引数を使用できます。すべての引数は、これらの名前付きイベント用に登録したリスナー関数内で使用できます。
たとえば、データイベントを操作するには、登録されたリスナー関数が、生成されたイベントに渡されたデータ引数にアクセスします。 そして、このデータオブジェクトはasyncFunc
提供するものとまったく同じです。
withTime.on('data', (data) => { // do something with data });
通常、 error
イベントは特別です。 コールバックのある例-リスナーを使用してエラーイベントを処理しない場合、Nodeプロセスは終了します。
この動作を実証するには、不正な引数を指定してメソッド実行を再度呼び出します。
class WithTime extends EventEmitter { execute(asyncFunc, ...args) { console.time('execute'); asyncFunc(...args, (err, data) => { if (err) { return this.emit('error', err); // Not Handled } console.timeEnd('execute'); }); } } const withTime = new WithTime(); withTime.execute(fs.readFile, ''); // BAD CALL withTime.execute(fs.readFile, __filename);
最初の実行呼び出しはエラーになります。 ノードプロセスがクラッシュまたは終了します。
events.js:163 throw er; // Unhandled 'error' event ^ Error: ENOENT: no such file or directory, open ''
このドロップは、2番目の実行呼び出しに影響しますが、まったく実行されない場合があります。
特別なerror
イベントのリスナーを登録すると、Nodeプロセスの動作が変わります。 例:
withTime.on('error', (err) => { // do something with err, for example log it somewhere console.log(err) });
この場合、最初の実行呼び出しのエラーが報告されますが、Nodeプロセスはクラッシュせず、終了しません。 2番目の実行呼び出しは正常に終了します。
{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' } execute: 4.276ms
Nodeはpromiseに基づいた関数では異なる動作をするようになりました。警告のみを表示しますが、最終的には変更されることに注意してください。
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open '' DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
生成されたエラーによる例外を処理する別の方法は、 uncaughtException
プロセスのグローバルイベントuncaughtException
を登録することです。 ただし、このようなイベントでグローバルにエラーをキャッチすることはお勧めできません。
uncaughtException
標準的なヒント:使用しuncaughtException
ください。 ただし、必要な場合(たとえば、何が起こったのかを報告したり、クリーンアップしたりする場合)、とにかくプロセスを終了させます。
process.on('uncaughtException', (err) => { // something went unhandled. // Do any cleanup and exit anyway! console.error(err); // don't do just that. // FORCE exit the process too. process.exit(1); });
ただし、いくつかのエラーイベントが同時に発生したとします。 これは、 uncaughtException
リスナーが数回uncaughtException
ことを意味し、コードをクリアするときに問題になる可能性があります。 これは、たとえば、複数の呼び出しによってデータベースがシャットダウンされる場合に発生します。
EventEmitter
モジュールは、 once
メソッドを提供します。 リスナーの1回の呼び出しで十分であることを通知します。 uncaughtException
を指定してメソッドを使用するのが実用的です。最初のキャッチされなかった例外では、いずれにしてもプロセスが終了することを認識して、クリーンアップを開始するためです。
リスナーの順序
1つのイベントが複数のリスナーを登録する場合、それらは何らかの順序で呼び出されます。 最初に登録されたものが最初に呼び出されます。
// प्रथम withTime.on('data', (data) => { console.log(`Length: ${data.length}`); }); // दूसरा withTime.on('data', (data) => { console.log(`Characters: ${data.toString().length}`); }); withTime.execute(fs.readFile, __filename);
このコードを実行すると、最初に「Length」という行がログに入力され、次に「Characters」が入力されます。これは、この順序でリスナーを決定するためです。
新しいリスナーを定義する必要があるが、最初にprependListener
必要がある場合は、 prependListener
メソッドを使用できます。
// प्रथम withTime.on('data', (data) => { console.log(`Length: ${data.length}`); }); // दूसरा withTime.prependListener('data', (data) => { console.log(`Characters: ${data.toString().length}`); }); withTime.execute(fs.readFile, __filename);
この場合、「Characters」という行がログの最初に表示されます。
最後に、リスナーを削除する必要がある場合は、 removeListener
メソッドを使用します。
それだけです