EventEmitterの代わりにEventEmitter2を使う

このエントリをNode.js Advent Calendarにするか迷いましたが、Advent Calendarとしてはちょっとマニアックだったので、没になった方もアップします。ブログエントリの大掃除中です。

Node.jsを使うならEventEmitterを知っとくべし、という記事とかありますが、EventEmitter使ってるといくつか疑問に当たりますよね。

EventEmitterおさらい

EventEmitterの基本的な機能をおさらいしておくと、EventEmitterというのは、イベントという単位で処理を行えるようにするためのモジュールで、イベントの受発信を行うことで非同期プログラミングを行いやすくするもの。

  • emitでイベントを発行
  • onで発行したイベントを受信
  • removeListenerでイベントを消すことができる
  • onceで一回だけイベントを受信することができる

で、こんな感じで使いますよね。

var EventEmitter = require('events').EventEmitter;
var util = require('util');

function EventTest() {
  // see http://d.hatena.ne.jp/Jxck/20110621/1308616949
  EventEmitter.call(this);
}

// EventTestはEventEmitterを継承する
util.inherits(EventTest, EventEmitter);

EventTest.prototype.log = function(data) {
  console.log(data);
}

var etest = new EventTest();


// onでイベント受信
etest.on("test", etest.log);

// onceだと一回だけイベント受信
etest.once("test_once", etest.log);

// emitでイベント発行
etest.emit("test", "Hello EventEmitter");

// removeListenerでイベントを消すことができる
etest.removeListener("test", etest.log);

// 削除されているとemitしてもイベントを受け取れない
etest.emit("test", "Hello EventEmitter");

// onceに対してemitする。
etest.emit("test_once", "Hello EventEmitter Once");

// onceだと2回emitしても受信されない。
etest.emit("test_once", "This message is not shown.");

EventEmitterでよくある質問

んで、EventEmitterでよくある質問としては、

これらの質問は僕もよくNode.jsの初心者から受けます。機能を追加する件に関して質問に回答すると、「現時点ではeventsのAPIはfreezeされてるので、Node.jsとしてはよっぽどの理由がない限り作る予定はない。」です。removeListenerのaliasとしてoffを最初から追加しなかった理由は分かりませんが、そこまでcallしないと思われてたんじゃないかなと思います。ワイルドカードもコアモジュールがはじめから用意するほど必要かと言われると疑問があります。

ただ、これらの要望が存在することも確か。offやワイルドカードを使いたかったらEventEmitter2を使いましょう。

EventEmitter2の特徴

  • offでremoveListenerできる
  • ワイルドカード対応
  • ネームスペースでフィルタすることができる。
  • onceで1回だけ受信、manyを指定すると回数がn回と指定できる
  • Browserでも使える。(bower対応してる)
  • EventEmitterよりも性能が良い


APIとしては、こんな感じですかね。

var EventEmitter = require('eventemitter2').EventEmitter2;
var util = require('util');

function EventTest(opts) {
  // see http://d.hatena.ne.jp/Jxck/20110621/1308616949
  EventEmitter.call(this, opts);
}

// EventTestはEventEmitterを継承する
util.inherits(EventTest, EventEmitter);

EventTest.prototype.log = function(data) {
  console.log(data);
}

// EventEmitter2でwildcard使うならオプションをtrueにする。
var etest = new EventTest({wildcard:true});


// onでイベント受信
etest.on("test", etest.log);

// onceだと一回だけイベント受信
etest.once("test_once", etest.log);

// manyで指定回だけイベント受信
etest.many("test_4times", 4, etest.log);

// wildcardを使える。
etest.on("name.*", etest.log);
etest.on("name.wildcard", etest.log);

// *で全部のイベントを受信するものも作れる。
//etest.on("*", etest.log);
// これとおなじものはonAnyでも作れる。
//etest.onAny(etest.log);

// emitでイベント発行
etest.emit("test", "Hello EventEmitter");

// offでイベントを消すことができる
// ちなみにEventEmitterと互換性があるのでremoveListenerでも消せる。
etest.off("test", etest.log);

// 削除されているとemitしてもイベントを受け取れない
etest.emit("test", "Hello EventEmitter");

// onceに対してemitする。
etest.emit("test_once", "Hello EventEmitter Once");

// onceだと2回emitしても受信されない。
etest.emit("test_once", "This message is not shown.");

// manyに対してemitする。
for (var idx=0; idx<4; idx++) 
etest.emit("test_4times", "Hello EventEmitter Many : " + idx);

// 指定回数を超えるとemitしてもイベントを受け取れない
etest.emit("test_4times", "This message is not shown.");

// name.*で受信しているイベントにemit
etest.emit("name.abc", "Hello EventEmitter wildcard abc");
etest.emit("name.def", "Hello EventEmitter wildcard def");

// emitの時にもwildcardが使える。
// この場合、name.*とname.wildcardの2つが発火する。
etest.emit("name.*", "Hello EventEmitter wildcard *");

EventEmitter2注意点

んで、ここでEventEmitter2を継承して使う時の注意点なんですが、EventEmitter2はコンストラクタでwildcardオプションやnamespaceのdelimiterをオプションとして取りうるので、以下のようにしておく必要があります。ここだけ通常のEventEmitterとは違うので注意。

var EventEmitter = require('eventemitter2').EventEmitter2;
var util = require('util');

// optsを指定できるようにする必要がある。
function EventTest(opts) {
  // optsをcallで渡す
  EventEmitter.call(this, opts);
}

// EventTestはEventEmitterを継承する
util.inherits(EventTest, EventEmitter);

browserで使える

EventEmitter2の特徴としてbrowserでも使えるように拡張している点が挙げられます。

EventEmitter2のソースコードを読むと分かると思いますが、

  if (typeof define === 'function' && define.amd) {
    // amdが定義されていたらEventEmitterを定義する。
    define(function() {
      return EventEmitter;
    });
  } else if (typeof exports === 'object') {
    // Node.jsとかCommonJSでloadできるようにする。
    exports.EventEmitter2 = EventEmitter;
  }
  else {
    // Browserのグローバル変数であるwindowに定義する。
    window.EventEmitter2 = EventEmitter;
  }

という感じにBrowserでも利用できるようになっています。

取ってくるときもbower対応しているので、以下の要領で取ってこれるかと。

$ bower install eventemitter2

EventEmitterよりも性能が良い

EventEmitterよりも性能が良いとされています、実際に自分の環境で10万ループ位回してDate.nowで検証してもそこまで有意差はありませんでした。ただ、EventEmitter2のbenchmarkスクリプトを使った時は以下の様な結果になりました。

// EventEmitterをwarming upさせるためのスクリプト
EventEmitterHeatUp x 1,340,107 ops/sec ±0.81% (97 runs sampled)

// 通常のEventEmitter
EventEmitter x 1,445,590 ops/sec ±0.86% (98 runs sampled)

// EventEmitter2
EventEmitter2 x 8,424,845 ops/sec ±0.94% (93 runs sampled)

// EventEmitter2 with wildcard
EventEmitter2 (wild) x 4,668,438 ops/sec ±7.79% (95 runs sampled)

Fastest is EventEmitter2

なんと、EventEmitter2の方が6倍程度高速という結果になりました。ソースコードの中を読むと分かりますが、そこまで特別な事をしている訳ではなく、割りと愚直に関数呼び出しを減らしたりとチューニングが施されてます。

ちなみにこのベンチマーク系を調査してたら realtime好きエンジニア、socket.ioのコミッタとしても有名な3rd-Eden作成のEventEmitter3というさらにチューニングされたものがありました。

そのベンチを回すと、EventEmitter3はよりチューニングされていて、EventEmitter2よりももう少しだけ早いです。

Starting benchmark run/emit.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (239707), Cycles (7), Elapsed (5.622), Hz (4597990.100779976)
 log:      Finished benchmarking: "EventEmitter 2"
 metric:   Count (224850), Cycles (6), Elapsed (5.411), Hz (4318758.747286304)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (302509), Cycles (5), Elapsed (5.471), Hz (5756023.847885787)
 info:     Benchmark: "EventEmitter 3" is was the fastest.

Starting benchmark run/init.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (977319), Cycles (5), Elapsed (5.409), Hz (18811591.703427445)
 log:      Finished benchmarking: "EventEmitter 2"
 metric:   Count (1134781), Cycles (6), Elapsed (5.523), Hz (21313177.940349065)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (1673912), Cycles (8), Elapsed (5.527), Hz (31928650.23817596)
 info:     Benchmark: "EventEmitter 3" is was the fastest.

Starting benchmark run/listeners.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (42060), Cycles (3), Elapsed (303.792), Hz (692.3598339245059)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (7264), Cycles (3), Elapsed (9.574), Hz (5331.132437877544)
 info:     Benchmark: "EventEmitter 3,EventEmitter 1" is was the fastest.

Starting benchmark run/listening.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (61266), Cycles (8), Elapsed (5.674), Hz (1183277.459347407)
 log:      Finished benchmarking: "EventEmitter 2"
 metric:   Count (67649), Cycles (6), Elapsed (5.543), Hz (1294856.9687415992)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (498828), Cycles (9), Elapsed (5.598), Hz (9468816.09925713)
 info:     Benchmark: "EventEmitter 3" is was the fastest.

Starting benchmark run/multiple-emitters.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (34135), Cycles (5), Elapsed (5.461), Hz (660708.2343786763)
 log:      Finished benchmarking: "EventEmitter 2"
 metric:   Count (43356), Cycles (7), Elapsed (5.555), Hz (834452.6059817146)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (58571), Cycles (7), Elapsed (5.453), Hz (1129233.8318203408)
 info:     Benchmark: "EventEmitter 3" is was the fastest.

Starting benchmark run/once.js

 log:      Finished benchmarking: "EventEmitter 1"
 metric:   Count (45142), Cycles (6), Elapsed (5.729), Hz (855865.1625286054)
 log:      Finished benchmarking: "EventEmitter 2"
 metric:   Count (34704), Cycles (5), Elapsed (5.517), Hz (670088.570910471)
 log:      Finished benchmarking: "EventEmitter 3"
 metric:   Count (343622), Cycles (7), Elapsed (5.442), Hz (6697614.402513785)
 info:     Benchmark: "EventEmitter 3" is was the fastest.

じゃあ速さは正義か、という話

まぁEventEmitter2やEventEmitter3の方が速いですが、速度だけで使うのは微妙な気もします。何万回、何百万回とメソッドコールした時には有意差が出ますがその時の有意差は通常のアプリケーションにおいてはそこまでの差は出ません。

通常のEventEmitterの方が使われているので変なバグを踏む可能性は低いですし、色んな配慮がされています。もちろん、EventEmitter2やEventEmitter3の方がOSSとしてコミットしやすく、Nodeのコアモジュールとは分離されているので何かの問題があった時にリリースサイクルが早くなる可能性がある、とは思います。

利点や不利点を踏まえて使いましょう。

とりあえず言えることは、offやワイルドカードを使いたいならEventEmitter2、EventEmitterの速度がシビアに求められる状況ならEventEmitter3を使うといいと思います。ただし、利用するならコアモジュールのEventEmitterと比較して安定していない可能性があることも考慮して使いましょう。