EventEmitterはいつ使うの?

非同期のJavaScriptにはCallbackパタンがよく使われてる。Node.js開発経験のある方ならわかると思うが、処理が長くなると、コードが階段状になりがちなのだ:

callme(function(done){
    callme(function(){
        callme(function(){
            callme(function(){
                done();
            });
        });
    });
});

Callbackの処理が簡単な場合は、この階段状のコードでも特に問題ないだが、もし複雑の処理になると、かなり読みづらい。そこで、EventEmitterを使って処理結果をEventとして外に出すのが解決方の一つ。

EventEmitterの使い方

EventEmitterはnode.jseventsモジュールの中に入ってる。EventEmitterを継承することで、onemitメソードでEventの受け取りと転送ができる。サンプルを作成してみよう:

var util = require('util');
var events = require('events');
var fs = require('fs');
var async = require('async');

// Event名が正しさを確保するために、変数を使う
var _e = {
    data : 'data',
    end : 'end',
    error: 'error',
};

// DirReaderクラス
function DirReader(dir){
    events.EventEmitter.call(this);
    // dirをセット
    this.dir = dir;
}
util.inherits(DirReader, events.EventEmitter);

DirReader.prototype.read = function() {
    var self = this;
    var count = 0;
    // ディレクトリーに入ってるファイルを読み出す
    fs.readdir(this.dir, function(err, files) {
        // エラーを直接throwするより、「error」イベントとして外にだす
        if (err) return self.emit(_e.error, err);

        // すべてのファイル中身を順番に読み出す
        async.eachSeries(files, function(file, done) {
            fs.readFile(file, function(err, data) {
                if (err) return done(err);

                // 無事に読み出せば、データを「data」イベントとして外にだす
                self.emit(_e.data, data);
                count++;
                done();
            });
        }, function(err) {
            if (err) return self.emit(_e.error, err);

            // 処理終了を「end」イベントとして出す。
            self.emit(_e.end, count);
        });
    });
};

このDirReaderモジュールは下記のように使える:

var reader = DirReader('./path');
reader.on('data', function(data){
    console.log(data);
});
reader.on('end', function(count){
    console.log('ファイル' + count + '個を処理しました。');
});
reader.on('error', function(err){
    console.log(err);
});

処理結果をイベント形式で外に出すと、複雑な処理をやってもコードの可読性を保てる。実はnode.jsのapiにもこのパターンが大量に使われている。例えば、streamとか〜

onのほかにはonceというメーソドがある。onと違って、onceはそのイベントを一回だけ受け取って、そのあと同じイベントが転送してきたら、無視するという挙動になる。

EventEmitterの応用

EventEmitterを使う理由は、コードの可読性がよくなるだけじゃなく、同じイベントに複数の処理が行えるというメリットもある。例えば先ほどのサンプルで、DirReaderで読み出したデータをconsoleに出す同時に、データベースにも保存したい場合はどうするんだろう?もう一個onを作ってdataイベントを購読すれば良いのだ:

var reader = DirReader('./path');
reader.on('data', function(data){
    // consoleにだす
    console.log(data);
});
reader.on('data', function(data){
    // データベースに入れる
    var file = new File({body: data});
    file.save();
});
reader.on('end', function(count){
    console.log('ファイル' + count + '個を処理しました。');
});
reader.on('error', function(err){
    console.log(err);
});

簡単でしょ?もちろん、consoleに出すなら、onを一個用意するほどでもないが、ニュアンスが伝えられたらと思う。

まとめ

EventÉmitterを使うことで、コードが読みやすくなるし、今後システムの拡張などにも十分な柔軟性を持つので、node.jsでの開発を次のレベルに向かうなら、ぜひEventEmitterを活用してください。