2020-05-16

【Node.js】EventEmitterとは何?いつ使うの?

EventEmitterの使い方をできるだけ分かりやすく解説する。

Article Image

よく見かける「.on

.on」というメソッド。Node.jsならfsSocket.ioで本当によく見かけるメソッドだ。

Node.jsでは**「event-driven architecture」**と呼ばれている概念。

EventEmitterというクラスからきているが、おそらく殆どの初心者は「なんとなくそういうものだ」として使っているのではないだろうか。

これは「イベントを発火する(emit)」と「対応したイベントの処理をする(on)」というシンプルなものだがそもそもなんでそんな面倒なことが必要なのだろうかというところにスポットライトを当ててみる。

前提

ここからはES6の構文で書いていく。拡張子は.mjs

また、この記事は少しむずかしい内容なので上から順番に読みすすめることを前提に書いている。

長くはなるがわかりやすくなるよう順を追っているので頑張ってみて欲しい。

まずはEventEmitterを理解する

次のコードを実行する

import EventEmitter from 'events'
const event = new EventEmitter();

console.log('1')
event.on('event', () => {
    console.log('2')
})
console.log('3')

event.emit('event')

実際のユースケースでこんなコードは書かないが、概念理解に最もシンプルな例を出した。

.onは「イベントを待つ」処理なので

  1. まず console.log('1')
  2. .onメソッドはイベントが発火するまで処理されないのでスルー
  3. console.log('3')
  4. 'event'を発火したのでonに戻ってconsole.log('2')

つまり結果は次のようになる

1
3
2

これが基本だ。

関数で良いのでは?

ここでピンときた人もいるだろう。

上の例は次に書き換えられる。

console.log('1')
const event = () => {
    console.log('2')
}
console.log('3')
event()

結果は1,3,2で同じ。

このように出来るのになぜわざわざイベント・ドリブンな概念が生まれたのだろうか?

その前に

よくEventEmitterは「非同期処理と組み合わせて使われる」という記述を見かける。

そこでまずは例を作って順を追って理解していく。

event()を1秒毎に繰り返す処理を使って非同期を考える。

console.log('1')
const event = () => {
    console.log('2')
}
console.log('3')
setInterval(event,1000) //1秒毎にeventを繰り返す

まだEventEmitterは出てきていない。

実行結果は

1
3
2
2
2
2
... 永遠に続く

これはこれで良い。当然だ。

クラス化すると・・・?

ではこれをクラス化する。

class myClass {
    start() {
        console.log('1')
        console.log('3')
        setInterval(this.event, 1000) //1秒毎にeventを繰り返す
    }
    event() {
        console.log('2')
    }
}

const test = new myClass()
test.start()

これで上のコードと全く同じ動作をするクラスができた(延々と1秒毎に2が出力される)

「2」固定ではなくユーザーが自由に文字列を入れたい

すこしクラスを変更してみる。この例は簡単だ。

今回は「Hello」を表示する。

class myClass {

    start(msg) {
        console.log('1')
        console.log('3')
        setInterval(() => {
            this.event(msg)
        }, 1000) //1秒毎にeventを繰り返す
    }
    event(msg) {
        console.log(msg)
    }
}

const test = new myClass()
test.start('Hello')

実行結果

1
3
Hello
Hello
...

ここまでは簡単

さて、ここまでは誰でも理解できる(はず)だ。

ここからが問題。

文字を変えるのではなく処理を変えたい!?

さて、先程は「2」と表示されたものを「Hello」の表示に変えただけだ。

つまり引数が変わっただけでconsole.logという処理自体は変わってない。

では、ここで処理を変えたい場合はどうしたら良いだろうか。

先程「関数で良いのでは?」と言った通り、関数で実装してみる。

例:Helloのあとに日付を表示する処理を入れる

日付表示する処理を追加してみよう。

つまりクラスを書き換えて・・・


class myClass {
    start(msg) {
        console.log('1')
        console.log('3')
        setInterval(() => {
            this.event(msg)
        }, 1000) 
    }
    event(msg) {
        console.log(msg)
        console.log(new Date()) //これを追加
    }
}

const test = new myClass()
test.start('Hello')

クラスの関数に1行追加した。

これは正常動作する。つまり目的は達成している。

あれ、この変更はマズいのでは!?

この例の問題点に気づいただろうか?

そう。ここでは日付を表示するという目的は達成しているが、あくまでこれは「クラス」なので別のプログラムからも呼び出される設計でなければならない。

つまりこのクラスを利用している他のプログラム上にも現在の日付が表示されてしまう変更が施されることになる。

これは始めからそういう設計なら問題ないが、場合によってはエラーの元となる。

親の関数を呼び出したい?

もうピンときた人もだろうか?

つまりこの問題を解決するにはクラスの中に処理を追加するのではなく、クラスを呼んでいる親側の関数を呼び出してやることができれば各プログラムごとに別々の処理がきるわけだが・・・

ようやく最初の「関数で良いのでは?」の項の答えとなる。この例では「関数ではダメ」なのだ。親側の関数は直接呼び出すことは出来ない。

ようやくここでイベントエミッターの登場だ。

import EventEmitter from 'events'

class myClass {
    constructor(){
        this.eventEmitter = new EventEmitter() //イベントエミッターを生成
    }
    start(msg) {
        setInterval(() => {
            this.event(msg)
        }, 1000) 
    }
    event(msg) {
        console.log(msg)
        this.eventEmitter.emit('event') //emitでイベントを発火
    }
}

// --------- 実際は上を別ファイルとしていろいろなプログラムから読み込む ---------

const test = new myClass()
const event = test.eventEmitter //イベントエミッターをもらう
test.start('Hello')

event.on('event',()=>{ //onで発火したイベントを受け取る
    console.log(new Date()) //ここに自由に追加の処理を書く
})

※余計なconsole.logは削った。

実行結果

Hello
2020-05-16T05:30:59.798Z
Hello
2020-05-16T05:31:00.800Z
Hello
2020-05-16T05:31:01.802Z
...

これで完成

これでクラスの中で発火したイベントを親が受け取って、それに応じて親側が好きな処理を実行するという仕組みが完成する。

今回のプログラムでは日付を表示しているが、別のプログラムでは計算をしたり、ファイルを読んだり・・・なんでもできるというわけだ。

またemitの第2引数以降に入れてやったデータはonで受け取れるので変数の受け渡しも可能だ。

一応よくありそうなユースケースを考えて非同期風コードにしたが、そもそも非同期でなければいけないという制約はないので自由につかってみてほしい。

乱暴なまとめ

クラス化+非同期+別々の処理

ときたらイベントエミッターを思い浮かべれば良いだろう。

Socket.ioの例を考えてみると

  • Socket.ioというクラス
  • 通信は非同期で何回も発生する
  • ユーザによって送受信するデータの処理は異なる

まさにイベント・ドリブンの概念を使うならコレという印象だ。

またVue.jsでは親と子のイベント/データの受け渡しが頻繁に行われるため$emitv-onは良く使われる。厳密には違うものだが概念的には似ているのでしっかり理解しておきたい。

しかしこれ以外のユースケースも色々あるかもしれない。思いついたら記事にしたい。

さいごに

今後ビッグデータを扱う仮想通貨のバックテスト機能を実装していくにあたってデータが大きすぎるためファイル処理によるバッファを考える可能性が出てきた。

それに合わせてファイル処理(fs)のストリームを考えることになるため.onメソッド等の正しい理解を整理しておこうと思いこの記事の執筆に至った。

特にこの次に執筆予定のEventStreamというライブラリでは内部でEventEmitterの例も記述してあるのでこのあたりの理解は必須といった印象だ。

EventStreamに至っては日本語解説が全くと行っていいほどされていないのでドキュメントをしっかり読んでいきたい。

一連の流れの最初の一歩だと思って読んで頂けたら幸いである。

さて、基本の基本で1記事消費することになったがここからの道のりは長くなる。頑張っていきたい。

おまけ

親の関数が呼び出せない・・・というのは厳密には嘘だ。

次のコードはイベントエミッターを使わずに同じ動作をする。

class myClass {
    start(msg, cb) {
        console.log('1')
        console.log('3')
        setInterval(() => {
            this.event(msg, cb)
        }, 1000) //1秒毎にeventを繰り返す
    }
    event(msg, cb) {
        console.log(msg)
        cb()
    }
}

const cb = () => {
    console.log(new Date()) //ここに自由に追加の処理を書く
}

const test = new myClass()
test.start('Hello', cb)

関数そのものを直接クラスに渡してしまえばクラスの中で親の関数を呼べる。所謂コールバックと呼ばれる使い方だ。

コールバックはJavaScriptでは**「コールバック地獄」**と呼ばれ可読性が悪くバグの温床になる可能性が高くなるということが分かっている。詳しくは書かないが基本的には「避けられるている」使い方である。

例えば今回の例で言えばtest.startcbを引き渡すのを忘れたり、関数ではなく文字列を入れるとエラーとなり止まってしまうがEventEmitterの場合は.onのコードを書かなくてもエラーは出ない。



この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)