nodeコアモジュールを認識する--EventEmitterに深く入り込む

10949 ワード

原文は私のブログで、転載は出典を明記してください
Nodeはイベント駆動メカニズムを採用しており、EventEmitterはnodeがイベント駆動を実現する基礎である.EventEmitterに基づいて、nodeのほとんどのモジュールがこのクラスを継承し、非同期イベント駆動アーキテクチャを実現します.EventEmitterのモジュールを継承し、独自のイベントを持ち、リスナーをバインド/トリガでき、非同期操作を実現しました.EventEmitterはnodeイベントモデルの基礎であり、EventEmitterに基づいて構築されたイベント駆動アーキテクチャは非同期プログラミングの思想を体現しているため、nodeプログラムを構築する際にもこのような思想に従わなければならない.EventEmitter実装の原理はオブザーバーモードであり,これもイベント駆動を実現する基本モードである.本文はEventEmitterをめぐって,その原理観察者モデル,体現する非同期プログラミング思想および応用を検討する.

本文


eventsモジュールのEventEmitterクラス


Nodeのeventsモジュールは、node非同期イベント駆動アーキテクチャの基本モードであるオブザーバーモードを実現するEventEmitterクラスのみを提供し、バインドイベントやトリガイベントなどのイベントリスナーモードが一般的に提供するAPIを提供します.
const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()

function callback() {
    console.log(' event !')
}
myEmitter.on('event', callback)
myEmitter.emit('event')
myEmitter.removeListener('event', callback);

EventEmitterクラスを継承すれば、イベント、トリガイベントなどを持つことができ、すべてのイベントをトリガできるオブジェクトはEventEmitterクラスのインスタンスです.
オブザーバーモード(イベントパブリッシュ/サブスクリプションモード)はEventEmitterクラスを実現する基本原理であり、イベント駆動メカニズムの基本モードでもある.

イベント駆動原理いべんとくどうげんり:オブザーバモードオブザーバモードオブザーバモード


イベント駆動システムでは、イベントはどのように生成されますか?イベントが発生した場合、なぜコールバック関数を自動的に呼び出すことができますか?まず観察者パターンを見てみましょう.
オブザーバ(Observer)モードは、1つのオブジェクトの変化が他の複数のオブジェクトに通知され、これらのオブジェクト間でばらばらな結合が必要である場合に適用される設計モードである.このパターンでは,被観察者(主体)が他のオブジェクトから派遣(登録)された観察者のグループを維持し,新しいオブジェクトが主体に興味があれば観察者を登録し,興味がなければ購読をキャンセルし,主体が更新されると観察者たちに順次通知する.猿の話をすると:
function Subject() {
    this.listeners = {}
}

Subject.prototype = {
    //  
    addListener: function(eventName, callback) {
        if(typeof callback !== 'function')
            throw new TypeError('"listener" argument must be a function')

        if(typeof this.listeners[eventName] === 'undefined') {
            this.listeners[eventName] = []
        } 
        this.listeners[eventName].push(callback) //  
    },
    //  
    removeListener: function(eventName, callback) {
        if(typeof callback !== 'function')
            throw new TypeError('"listener" argument must be a function')
        if(Array.isArray(this.listeners[eventName]) && this.listeners[eventName].length !== 0) {
            var callbackList = this.listeners[eventName]
            for (var i = 0, len=callbackList.length; i < len; i++) {
                if(callbackList[i] === callback) {
                    this.listeners[eventName].splice(i,1)   //  
                }
            }
            
        }
    },
    //  : , 
    triggerEvent: function(eventName,...args) {
        if(this.listeners[eventName]) {
            for(var i=0, len=this.listeners[eventName].length; i

OK、リスナーを追加し、イベントを送信します.
var event = new Subject()
function hello() {
    console.log('hello, there')
}
event.addListener('hello', hello)
event.triggerEvent('hello')     //    hello, there
event.removeListener('hello', hello) //  
setTimeout(() => event.triggerEvent('hello'),1000) //  

オブザーバモードでは、登録されたコールバック関数、すなわちイベントリスナーが、イベント呼び出しの各コールバック関数、すなわちパブリッシュメッセージをトリガする.
観察者モードは、信号対応関数のリストを維持するだけで、保存することができ、除去することができ、信号(インデックス)を与えるだけで、この信号に従って対応する関数を実行し、間接呼び出しに相当することがわかります.それは直接関数を呼び出せばいいのではないでしょうか.どうしてそんなに曲がりくねって書いたのですか.先ほども述べたが,これは観察者モードがオブジェクト間の関係をデカップリングし,表現層とデータ論理層の分離を実現し,安定した更新メッセージングメカニズムを定義できるためである.
最初の質問に戻ると、イベントはどのように生成され、自動的に呼び出されますか?上記のようにevent.triggerEventが呼び出されたときに発生したのですか?いいえ、呼び出しevent.triggerEventは、コールバック関数を呼び出すことに相当し、イベント実行プロセスであり、イベント生成プロセスは、より多くの下位層によって生成され、nodeに通知される.nodeのグローバル変数processを例に挙げます.processはEventEmitterの例です.
process.on('exit', (code) => {
  console.log(`About to exit with code: ${code}`);
});

nodeが実行されるとprocessのexitイベントに指定したコールバックがバインドされ、上のaddListenerが呼び出されたことに相当します.プロセスを終了すると、指定した関数が実行されていることがわかりますが、exitイベントをトリガーする方法、つまり上のtriggerEventを手動で呼び出す方法はありません.これは、nodeの下部が呼び出されたからです.オペレーティングシステムの下部がこのプロセスを終了させ、nodeがこの情報を得るからです.次に,あらかじめ定義されたトリガメソッドをトリガし,コールバック関数を順次実行した.このような内蔵イベントはnodeモジュールが事前に作成してオープンしており、使用時にコールバック関数を直接バインドすればよいので、イベントをカスタマイズするには、自分で信号を送信しなければなりません.
上のコードは最も基本的な観察者モードを実現しており、nodeソースコードのEventEmitterの実現原理はこれとあまり差がなく、これらに加えて他の有用な特性が加えられており、様々な実現ができるだけ性能の良い方法を使用している(nodeソースコードは知恵の光を反映している).
Nodeの多くのモジュールは、ファイルモジュールシステムのFSWatcherのようなEventEmitterを継承しています.
const EventEmitter = require('events')
const util = require('util')
...

function FSWatcher() {
  EventEmitter.call(this);//  
  ...
}
util.inherits(FSWatcher, EventEmitter); //   EventEmitter

他のモジュールもそうです.これらはnodeの非同期イベント駆動アーキテクチャを構成します.

ひどうきプログラミングパターン


イベントモデルと非同期I/Oを用いるため,nodeにおける多数のモジュールのAPIは非同期コールバック関数の方式を採用しているため,下層にも非同期プログラミングの方式が随所に体現されていることがわかる.非同期にも多くの問題があります.理解が困難で、コールバックのネストが深すぎる、エラーが捉えにくい、マルチスレッドのプログラミングが困難であるなどですが、非同期による高性能に比べて、これらの問題に加えて比較的良い解決策があり、非同期プログラミングのモデルは試してみる価値があります.特にnodeを利用してアプリケーションを構築する場合です.
基本的なコールバック関数から
コールバック関数は非同期プログラミングの体現であり,コールバック関数の実現は高次関数から離れられない.Javascript言語の柔軟性のおかげで、関数はパラメータまたは戻り値として、関数をパラメータまたは戻り値とする関数は高次関数です.
function foo(x,bar) {
    return bar(x)
}//  foo, bar 

var arr = [2,3,4,5]
arr.forEach(function(item,index){
    // do something for every item
}) //  

event.addListener('hello', hello) //  addListener

高次関数の特性に基づいて,コールバック関数のモードを実現できる.実際、javascript関数の使い方が非常に柔軟であるため、高次関数と多くの設計モードがあります.
イベントパブリッシュ/サブスクリプションモード(オブザーバモード)の使用
単純に高次関数特性を使用すると、単純で柔軟で強力な非同期プログラミングモードのアプリケーションを構築するのに十分ではありません.他の言語からいくつかの設計モードを参考にする必要があります.上述したように、nodeのeventsモジュールは、非同期プログラミングに広く用いられるモードであるイベントパブリケーション/サブスクリプションモードを実現する.コールバック関数をイベント化し、イベントを各コールバック関数に関連付けます.コールバック関数を登録することは、イベントリスナーを追加することです.これらのイベントリスナーは、イベントと処理ロジック(登録されたコールバック関数)の間で簡単に関連付けられ、デカップリングされます.イベントパブリッシャは、リスナーがビジネスロジックをどのように実現しているのかに注目する必要はありません.また、イベントリスナーがどれだけあるかにも注目する必要はありません.メッセージに従って実行するだけでよく、データはこのようなメッセージを通じて柔軟に伝達することができます.
それだけでなく、このモードはクラスのように機能をカプセル化することもできます.不変の論理を内部にカプセル化し、カスタムで変化しやすい部分をイベントを通じて外部定義に暴露する必要があります.ノードの多くのオブジェクトにはこのような黒い箱の特徴があり、イベントフックを通じて、使用者がこのオブジェクトがどのように起動したのかに注目せず、自分が注目しているイベントに注目すればよい.
ほとんどのnodeコアモジュールのようにEventEmitterを継承すると、このモードを使用して、非同期プログラミングでnodeプログラムを構築することができます.
Promiseの利用
PromiseはCommonJsが発表した仕様で、非同期プログラミングに便利さをもたらしています.Promiseは,非同期呼び出し,ネストコールをカプセル化しただけで,本来複雑なネスト論理が不明なコールバックを優雅に理解しやすくした.Promiseのパッケージがあれば、非同期呼び出しと書くことができます.
function fn1(resolve, reject) {
    setTimeout(function() {
        console.log(' : ');
        resolve('1');
    },500);
}

function fn2(resolve, reject) {
    setTimeout(function() {
        console.log(' : ');
        resolve('2');
    },100);
}

new Promise(fn1).then(function(val){
    console.log(val);
    return new Promise(fn2);
}).then(function(val){
    console.log(val);
    return 33;
}).then(function(val){
    console.log(val);
});

ではPromiseはどのようにパッケージされていますか?
まず、Promiseは非同期、遅延操作の処理によく用いられ、thenの中に入れる「次にすること」を正しい順序で実行するために、Promiseはステータスマシンとして設計され、ステータスがpending=>resolve(成功)、pending=>reject(失敗)に変化するまた、Promiseは、成功または失敗したときに実行する関数Listを維持し、ListのコールバックはPromiseがpending状態にあるときにthenに登録されたコールバックpushである.Promiseの内部にはresolveとreject関数があり、それぞれ成功/失敗時に関数Listを実行し、この2つの関数はコールバック関数に渡され、ユーザーがいつresolve/rejectを決定するかを決定します.チェーン呼び出しを実現するためにthenで返されるのはpromiseです.
function getUserId() {
    return new Promise(function(resolve, i) {
        // 
        setTimeout(function(){
            console.log(' , promise '+i+' resolve')
            resolve('Fuck you Promise!', i)
        },1000)
    }, 1)
}

getUserId().then(function(words) {
    console.log(words)
})

//  
function Promise(fn, i) {
    var i = i
    var state = 'pending'
    var result = null
    var promises = []
    console.log('Promise' + i + 'constructing')

    this.then = function(onFulfilled) {
        console.log('then ')
        return new Promise(function(resolve) {
            console.log(' promise')
            handle({
                onFulfilled: onFulfilled || null,
                resolve: function(ret, i) {resolve(ret,i)}
            })
        },2)
    }

    function handle(promise) {
        if(state === 'pending') {
            console.log('promise' + i + ' pending ')
            promises.push(promise)
            console.log(' ')
            return
        }

        if(!promise.onFulfilled) {
            console.log(' ,resolve ')
            promise.resolve(result, i)
            return
        }
        console.log(' ')
        var ret = promise.onFulfilled(result)
        console.log(' ( promise)')
        promise.resolve(ret, 2)
        
    }

    function resolve (newResult, i) {
        console.log(' promise' + i + ' resolve')
        if(newResult && (typeof newResult === 'object' || typeof newResult === 'function')) {
            console.log('then promise')
            var then = newResult.then
            if(typeof then === 'function') {
                console.log(' then')
                then.call(newResult, resolve)
            }
        }
        console.log(' promise' + i + ' fulfilled')
        state = 'fulfilled'
        result = newResult
        setTimeout(function(){
            console.log(' promise' + i + ' ')
            console.log(promises[0])
            promises.forEach(function(promise) {
                handle(promise)
            });
        },0)
        
    }
    console.log(' resolve promise' + i + ' ')
    fn(resolve, i)
}

注意、これはPromise/A+仕様の簡単な実現であり、reject原理も同じである.私はここでpromiseをよりよく理解するために、混乱することはありません.ラベルを入れて、分かりやすくしました.Promise/A+規範にはありません.
実際、nodeの高バージョンはpromiseをサポートしており、直接使用できますが、Bluebirdのような三方ライブラリほど速くなく、BluebirdはPromise/A+にはない方法を多く拡張しています.
サードパーティ製ライブラリAsync/stepの使用
asyncは有名なプロセス制御ライブラリであり、npm installによく使用され、非同期コラボレーションモードを処理するのに20以上の方法を提供しています.例:
  • series-非同期タスクのシリアル実行は、Promiseのように形式が異なる
  • にすぎません.
  • parallel-非同期タスクはPromiseに相当する並列に実行される.all
  • waterfall--依存関係を持つ非同期呼び出しを処理する.例えば、前の結果が後の入力
  • である.
  • auto-非同期呼び出しの依存関係を自動的に解析します.パラメータは依存関係オブジェクト
  • です.
  • ...

  • Stepはasyncより軽量で簡単で、1つのインタフェースStepしかなく、インタフェースでStepが提供する方法を呼び出すことができ、機能はasyncとあまり差がありません.
    非同期プログラミングのパターンはこれだけではなく、多くの重要な思想、設計モデルがあり、実践の中で発見し、総括する必要がある.

    まとめ


    EventEmitterが提供するインタフェースは非常に簡単ですが、その背後にある思想はノード全体のアーキテクチャを貫いています.Nodeは非同期プログラミングを使用する最初のプラットフォームではありませんが、非同期アーキテクチャはNodeの中でどこでも体現されており、Node設計の基本思想です.nodeを学ぶ時、現象を通じて本質を見て、深く浅く浅く、賢明な方法で、いかなる物事に対してもそうです.
    参考文献:
  • https://segmentfault.com/a/1190000009478377
  • 【朴霊】『深入浅出Node.Js』