EventEmitter はNode.jsの events モジュールにあるクラスで、オブジェクトが名前付きのイベントを発行できるようにします。emitter.on('name', fn) でリスナーを登録し、emitter.emit('name', ...args) で発火させます。その名前のリスナーがすべて、追加した順に、渡した引数で実行されます。HTTPサーバー、ストリーム、process はどれもEventEmitterなので、同じいくつかのメソッドがNode.jsのどこでも使えます。
2つのリスナーはどちらも before emit と after emit の間で実行されます。emit はリスナーを同期的に呼び、それが終わってから戻るからです。2つの shop.on の呼び出しを入れ替えると、真ん中の2行も入れ替わります。
on、once、off
on は削除するまでリスナーを残し、once は最初の呼び出しの後で削除し、off(removeListener と同じ)は1つを削除します。リスナーを削除するには追加したときと同じ関数が必要なので、アロー関数をその場で渡さずに名前を付けておきましょう。
first open ever はAdaの後に1回だけ表示されます。logOpen を削除すると誰も聞いていないので、最後の emit は false を返します。door.removeAllListeners('open') を使えば、両方のリスナーを1回で消せました。
| メソッド | 動作 |
|---|---|
on(name, fn)、addListener | emitのたびに fn を呼ぶ |
once(name, fn) | 次のemitのときだけ fn を呼ぶ |
prependListener(name, fn) | on と同じだが、既存のリスナーより先に実行される |
off(name, fn)、removeListener | その関数を削除する |
emit(name, ...args) | リスナーを呼び、1つでもあれば true を返す |
listenerCount(name) | そのイベントのリスナーの数 |
errorイベント
'error' は特別です。誰も聞いていないのに発行すると、emit はそのエラーを投げます。実際のプログラムでは、捕まえられなかった例外はプロセスをクラッシュさせます。ストリームやサーバーのように失敗しうるEmitterには、必ず 'error' のリスナーを追加しましょう。
リスナーがないと emit が例外を投げ、catch がそれを受け取ります。リスナーを追加すると、同じ emit はリスナーを呼ぶだけで、プログラムはそのまま続きます。ほかのイベント名なら、リスナーがなくても黙って無視されます。
自作のクラスでEventEmitterを継承する
いちばんよくある使い方は、自分のクラスで EventEmitter を継承し、コンストラクタで super() を呼び、何か起きたら this.emit することです。呼び出す側は、Node.jsのほかのオブジェクトと同じようにあなたのオブジェクトを購読できます。
25%、50%、75%、100%、続いて photo.png uploaded が表示されます。up.start() を2つの up.on の行より上に移すと、何も表示されません。誰かが聞き始める前にイベントが発行されてしまうからです。
コンストラクタの中から発行するときも同じ落とし穴があります。すぐに発行しなければならないライブラリは、その呼び出しを process.nextTick で包みます。これは呼び出し側の on の行より後に実行されます(イベントループのページを参照)。
events.onceでイベントを待つ
events.once(emitter, name) は、次にイベントが発行されたときにその引数を配列にして解決するPromiseを返します。一度きりのイベントを await できる形に変え、先に 'error' が発行された場合はrejectします。
await はタイマーが ready を発行するまで止まり、その後で配列が2つの引数に分割代入されます。何度も発行されるイベントなら、events.on(emitter, name) が for await で回せる非同期イテレーターを返します。
よくある間違い
頻繁に実行される関数の中でリスナーを追加する。 リクエストハンドラーのたびに on を呼ぶと、リスナーとメモリが積み上がります。追加は1回にするか、once を使いましょう。
その場で書いた関数を削除しようとする。 off('data', () => ...) は何も削除しません。そのアロー関数は新しく作られた別の関数だからです。追加した関数への参照を持っておきましょう。
リスナーが後で実行されると思う。 emit は同期的なので、遅いリスナーは発行した側のコードを止めます。リスナーを async にしても解決しません。最初の await までは、そのコードは同じスレッドで動くからです。重い処理は間に setImmediate を挟んで小分けにするか、ワーカースレッドに移しましょう。
よくある質問
EventEmitterは非同期ですか?
いいえ。emit はすべてのリスナーをその場で1つずつ順に呼び、最後のリスナーが終わると戻ります。リスナーの中のコードが非同期の処理を始めることはできますが、リスナー自体は同期的に呼ばれます。
MaxListenersExceededWarningはどういう意味ですか?
1つのイベントに11個以上のリスナーが追加されると、Node.jsは警告を出します。リクエストのたびにリスナーを追加して削除していないことが多いからです。余分なリスナーを削除するか、本当に必要なら emitter.setMaxListeners(n) を呼びます。
emitは何を返しますか?
イベントにリスナーが1つ以上あれば true、誰も聞いていなければ false です。リスナーが返した値は返しません。
Node.jsのどのオブジェクトがEventEmitterですか?
Node.jsのコアオブジェクトの大半です。HTTPサーバー、リクエストとレスポンス、ストリーム、ソケット、process、readline のインターフェース。だからどれも on、once、off を共通で持っています。