課題解決
INSIGHT
情報・インサイト
Node.jsのEventEmitterでイベント駆動プログラミングをする
はじめに
今回はNode.jsでイベント駆動のプログラミングを行う際に使われる、EventEmitterを紹介します。
イベント駆動のプログラミングとは、順次に処理が実行されるのではなく、なんらかのイベントによってあらかじめ指定された処理が実行されるようなプログラミングの手法です。
クライアントサイドのJSに馴染みがある方は、DOM要素のイベントにコールバックを設定した事があると思いますが、それがまさにイベント駆動のプログラミングです。
今回はサーバーサイドのJSで、イベント駆動のプログラミングを行う方法を解説します。
EventEmitter とは
Node.js のコアライブラリで、イベント駆動のパラダイムでのプログラミングをサポートするライブラリです。
EventEmitter 自体のインスタンスを生成して使ったり、任意のクラスで継承して使うことができます。
EventEmitter を継承することで、event に listener を登録する EventEmitter#on や、event を発火させる EventEmitter#emit が利用できるようになります。
イベント駆動が必要な多くのライブラリで用いられています。
EventEmitter の API
- EventEmitter#defaultMaxListeners
- EventEmitter#on(eventName, listener)
- EventEmitter#once(eventName, listener)
- EventEmitter#emit(eventName, […args])
- EventEmitter#eventNames()
- EventEmitter#getMaxListeners()
- EventEmitter#setMaxListeners(n)
- EventEmitter#listeners(eventName)
- EventEmitter#listenerCount(eventName)
- EventEmitter#prependListener(eventName, listener)
- EventEmitter#prependOnceListener(eventName, listener)
- EventEmitter#removeAllListener(eventName)
- EventEmitter#removeListener(eventName, listener)
EventEmitter#on
listner を登録するために使われるメソッドです。
第 1 引数の eventName に紐づけて、第 2 引数の listener を登録します。
- eventName
- listener
EventEmitter#emit
event を発火させるために使われるメソッドです。
第 1 引数の eventName に紐づく event に登録されている全ての lisnter を、同期的に呼び出します。
その際には、第 2 引数以降の args を引数として渡し、返り値は無視します。
また、呼び出される順番は、単に登録された順番です。
- eventName
- [args]
EventEmitter#once
一度だけ呼び出される listener を登録するために使われるメソッドです。
EventEmitter#on と同様、第 1 引数の eventName に紐づけて、第 2 引数の listener を登録します。
このメソッドを使って登録された listener は、1 度実行されたら、2 回目以降の event の発火では無視されます。
EventEmitter#eventNames
listener が登録されているすべてのイベントの名前を要素にもつ配列を返します。
要素の型は、文字列またはシンボルです。
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.on("foo", () => {});
emitter.on("bar", () => {});
console.log(emitter.eventNames()); // Prints: [ 'foo', 'bar' ]
EventEmitter#listeners
第 1 引数の eventName に紐づくすべての listner を返します。
EventEmitter#listenerCount
第 1 引数の eventName に紐づくすべての listner の数を返します。
EventEmitter#prependListener
第 1 引数の eventName に登録されている listeners の内部リストの先頭に第 2 引数の listener を追加します。
EventEmitter#removeAllListeners
第 1 引数の eventName に登録されている全ての listeners を取り除きます。
EventEmitter#removeListener
第 1 引数の eventName に登録されている、第 2 引数の listener を取り除きます。
状況ごとのサンプルコード
イベントの登録と発火
EventEmitter#on で登録した listner を EventEmitter#emit で呼び出します。
const events = require("events"),
emitter = new events.EventEmitter(),
username = "kotaro",
password = "pass";
emitter.on("userAdded", (username, password) => {
console.log(`Added user: ${username}`);
});
emitter.emit("userAdded", username, password);
this の扱い
EventEmitter#on で登録された listner の内部のthisは、通常は付属の EventEmitter を指します。
ただし、Allow 関数を使うことで、thisを束縛することができます。
const events = require("events"),
emitter = new events.EventEmitter();
emitter.on("event", function() {
console.log("function listener:");
console.log(this); // EventEmitter ...
});
emitter.on("event", () => {
console.log("allow function listener:");
console.log(this); // {}
});
emitter.emit("event");
同期実行と非同期実行
EventEmitter は、全ての listener を登録された順に同期的に呼び出します。
これは、イベントに順序付けを行うことで、競合またはロジックエラーを回避するです。
ただし、適切な場合にsetImmediate()やprocess.nextTick()を呼び出すことで、listner を非同期に実行することもできます。
const events = require("events"),
emitter = new events.EventEmitter();
emitter.on("event", () => {
setImmediate(() => {
console.log("this happens asynchronously");
});
});
emitter.emit("event");
listner を一度だけ実行する
EventEmitter#on を使って登録された listner は、その event が発火されたタイミングで、何度でも実行されます。
もしも、listner を一度だけ実行したい場合、EventEmitter#once を使って listener を登録します。
EventEmitter#on で登録した場合
const events = require("events"),
emitter = new events.EventEmitter();
let count = 0;
emitter.on("count", () => {
console.log(++count);
});
emitter.emit("count"); // Prints: 1
emitter.emit("count"); // Prints: 2
EventEmitter#once で登録した場合
const events = require("events"),
emitter = new events.EventEmitter();
let count = 0;
emitter.once("count", () => {
console.log(++count);
});
emitter.emit("count"); // Prints: 1
emitter.emit("count"); // Ignored
Error イベント
EventEmitter のインスタンス内部でエラーが起きた場合、errorイベントが発火されます。
これは、Node.js に特殊なケースとして処理されます。
error イベントに登録された listener が存在しない場合
errorイベントが発火されると、error が投げられ、スタックトレースが出力され、Node.js のプロセスが終了します。
const events = require("events"),
emitter = new events.EventEmitter();
emitter.emit("error", new Error("whoops!"));
// Throws and crashes Node.js
error イベントには listener を登録する
少なくとも 1 つの listener を、errorイベントに登録しておくことが推奨されます。
errorイベントに登録された listner は、エラー発生時のオブジェクトを第 1 引数で受け取ることができます。
const events = require("events"),
emitter = new events.EventEmitter();
emitter.on("error", err => {
console.error("whoops! there was an error");
});
emitter.emit("error", new Error("whoops!"));
// Prints: whoops! there was an error
newListener イベント
EventerEmitter は、リスナーを追加する前に newListner イベントを発火します。
この時、新たに登録される eventName と、listener が、newListener イベントに登録されている listener に渡されます。
const events = require("events"),
emitter = new events.EventEmitter();
emitter.on("newListener", (event, listener) => {
console.log(`${listener} will be added to ${event}`);
});
emitter.on("event", () => {
console.log("one");
});
emitter.on("event", () => {
console.log("two");
});
/*
() => {
console.log('one');
} will be added to event
() => {
console.log('two');
} will be added to event
*/
const events = require("events"),
emitter = new events.EventEmitter();
// 無限ループしないために一度のみ実行する。
emitter.once("newListener", (event, listener) => {
if (event === "event") {
// 新たなlistnerをイベントに登録
// この処理はAよりも前に実行される。
emitter.on("event", () => {
console.log("B");
});
}
});
emitter.on("event", () => {
console.log("A");
});
emitter.emit("event");
// Prints:
// B
// A
removeListener イベント
EventerEmitter は、リスナーが除去された後に removeListner イベントを発火します。
この時、直前に登録された eventName とlistener が、newListener イベントに登録されていた listener に渡されます。
イベント駆動のプログラミングのまとめ、いかがでしたでしょうか?
最後までお読みくださり、ありがとうございました!
■参考資料
- Node.js v9.3.0 Documentation:https://nodejs.org/api/events.html
- Node.js Event Emitter:https://www.slideshare.net/EyalV/event-emitter
☆☆☆ 新しいことにチャレンジしたい!仲間と一緒に成長したい!やりがいのある仕事で充実した毎日を過ごしたい!そんな熱意をお持ちの方、私たちと一緒に働きませんか?ウィズテクノロジーでは一緒に働いていただけるエンジニアを大阪・東京で募集しています。☆☆☆