Node.jsむベントアヌキテクチャに぀いお


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 

コヌド実行の結果に関しお泚意したいこず非同期はありたせん。



叀き良きコヌルバックず同様に、むベントが同期コヌドたたは非同期コヌドの特性であるこずを瀺唆しおいたせん。 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メ゜ッドを䜿甚したす。


それだけです



Source: https://habr.com/ru/post/J330048/


All Articles