.on
」「.on
」というメソッド。Node.js
ならfs
やSocket.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
は「イベントを待つ」処理なので
console.log('1')
.on
メソッドはイベントが発火するまで処理されないのでスルーconsole.log('3')
'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
が出力される)
すこしクラスを変更してみる。この例は簡単だ。
今回は「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
では親と子のイベント/データの受け渡しが頻繁に行われるため$emit
とv-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.start
にcb
を引き渡すのを忘れたり、関数ではなく文字列を入れるとエラーとなり止まってしまうがEventEmitter
の場合は.on
のコードを書かなくてもエラーは出ない。