LoginSignup
2
1

More than 3 years have passed since last update.

Node.jsのEventEmitterの使い方

Last updated at Posted at 2020-11-27

はじめに

Node.jsではファイルの読み込みやHTTPリクエストなど、非同期で行われる処理があります。
この非同期処理の終了を検知する方法の一つとして、EventEmitterが用意されています。
このEventEmitterについてまとめます。

環境

  • Node.js 12.18.3

EventEmitterの使い方

ローカルに保存されているファイルを読み込み、終了後に後続の処理を実行するサンプルを作り、まとめていきたいと思います。

EventEmitterの呼び出し

EventEmitterクラスはNode.jsのeventsモジュールに定義されています。

// eventsモジュールのEventEmitterの参照を取得
const { EventEmitter } = require('events');

ファイル読み込み関数の作成

続いてファイルを読み込む処理を書いていきます。

// 〜略〜

// fsモジュールの読み込み
const fs = require('fs');

// ファイルを読み込む処理
const readFile = () => {
    // EventEmitterのインスタンス生成
    const emitter = new EventEmitter();

    // target.txtファイルを読み込む
    fs.readFile('target.txt', 'utf8', (err, data) => {
        // エラー発生時、errorイベントを発行し、処理を終了する
        if (err) {
            return emitter.emit('error', err);
        }
        // 読み込みが完了したら、finishイベントを発行し、ファイルの中身を渡す
        emitter.emit('finish', data);
    });

    // この関数にメソッドチェーンできるようにEventEmitterのインスタンスを返す
    return emitter;
};

この関数で使っているemit()が、イベントを発行するメソッドです。
emit()に第1引数で渡したイベントが発行され、次に記述するon()で登録されたリスナーに通知される仕組みです。(第2引数以降はそのままリスナー関数に引数として渡されます)

readFile関数内でやっていることを、日本語でまとめます。
1. EventEmitterのインスタンスを生成
2. target.txtファイルを読み込む
3. ファイル読み込み中にエラーが発生した場合、errorイベントを発行
4. ファイル読み込み完了後、finishイベントを発行

readFileの呼び出し

次にこの関数を呼び出して、実際にファイルを読み込む処理、発行されるイベントにリスナーを登録する処理を追記します。

// 〜略〜

// readFile関数を呼び出し、各イベントに対してリスナーを登録
readFile()
    .on('finish', (data) => console.log('finishイベント', data))
    .on('error', (err) => console.error('errorイベント', err));

なお、EventEmitterでerrorイベントemit()する場合は、呼び出し時に必ずon('error')を指定するようにしてください。
errorイベント発行時にリスナーが登録されていない場合、Unhandled 'error' eventエラーで処理が落ちてしまいますので、ご注意ください。

実行

まずはそのまま実行します。

実行結果
errorイベント [Error: ENOENT: no such file or directory, open 'target.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'target.txt'
}

ちゃんとon('error')で登録したリスナーが動き、「そんなファイルないよ」とエラーを出力してくれました。

次に実行ファイルと同じディレクトリに、次の内容でtarget.txtを作成し、

target.txt
target

実行します。

実行結果
finishイベント target

今度はon('finish')で登録したリスナーが動き、ファイルの中身を出力してくれました。

その他のメソッドについて

once

onceは、onと同じくイベントに対してリスナーを登録するメソッドですが、一度イベントが通知されると解除されます。

removeListener(event, listener)

登録済みのリスナーを解除するメソッドです。

EventEmitterクラスの拡張

上記のreadFile()関数をクラス化し、より複雑な処理をさせたい場合は、EventEmitterクラスを継承して独自のメソッドを追記することができます。

先程作成した関数をクラスにし、読み込むファイルを複数追加できるように書き換えます。

const { EventEmitter } = require('events');
const fs = require('fs');

class ReadFile extends EventEmitter {

    constructor() {
        // EventEmitterのコンストラクタ呼び出し
        super();
        // 検索対象のファイル名
        this.files = [];
    }

    // 読み込み対象のファイルを追加する処理
    addFile(file) {
        this.files.push(file);
        return this;
    }

    // ファイル読み込み
    readFile() {
        this.files.forEach((file) => {
            fs.readFile(file, 'utf8', (err, data) => {
                // エラー発生時、errorイベントを発行し、処理を終了する
                if (err) {
                    return this.emit('error', err);
                }
                // 読み込みが完了したら、finishイベントを発行し、ファイルの中身を渡す
                this.emit('finish', data);
            });
        });

        return this;
    }
}

次に上のクラスを使ってファイルを読み込む処理を追記します。

// ReadFileのインスタンス生成
const rf = new ReadFile();
// 読み込み処理
rf.addFile('target.txt')
    .addFile('dummy.txt')
    .readFile()
    .on('finish', (data) => console.log('finishイベント', data))
    .on('error', (err) => console.error('errorイベント', err));

今回は先程作成したtarget.txtに加えて、わざとエラーを起こすように存在しないファイルdummy.txtも読み込み対象に加えています。

それでは実行してみます。

実行結果
errorイベント [Error: ENOENT: no such file or directory, open 'dummy.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'dummy.txt'
}
finishイベント target

dummy.txtを読み込むときはerrorイベントが発行され、target.txtを読み込んだときはfinishイベントが発行されてファイルの中身が表示されました。

このクラスを使ったパターンはNode.jsのServerクラスなどでも使われているようです。

より大きなファイルを読み込むサンプル

今度はEventEmitterを継承しているStreamを使って、先程のサンプルよりも大きいファイルを読み込むサンプルを作成していきたいと思います。

大きなファイルを読み込む場合、先程のサンプルのように「すべてのデータをバッファに読み込んでから後続の処理をする」という方式をとっていると、なかなか処理が帰ってこず、不安になったり、メモリを一気に消費してしまって、負荷の高い処理となってしまう、ということが起きてしまいます。

それを解決するためにStreamを使います。
Streamはデータが読み込まれるたびに通知されるので、すべてのデータの読み込みを待つことなく、処理を開始することができます。

サンプルの作成

巨大なテキストファイルをすべて大文字にして出力する、というサンプルを作成します。

// streamモジュールからTransform streamを取得
const { Transform } = require('stream');
const fs = require('fs');

// Transform streamを継承したクラスを作成
class ToUpper extends Transform {
    constructor() {
        super();
    }

    // chunkが送られてくるたびに実行
    _transform(chunk, encoding, callback) {
        // 内部のバッファにすべて大文字にした文字列をpush
        this.push(chunk.toString().toUpperCase());
        callback();
    }
}

// 置換クラスのインスタンス生成
const toUpper = new ToUpper();
toUpper.on('data', (chunk) => console.log(chunk.toString() + '\n\n\n'));

// Read streamを使って対象ファイルを読み込み
const rs = fs.createReadStream('target.txt');
// 一定量読み込み、次の処理にchunkを渡す
rs.on('data', (chunk) => toUpper.write(chunk));

上記のサンプルでは、まずTransform streamを実装するため、stream.Transformを継承したクラスを作成します。今回の趣旨からは外れるため細かな説明は省きますが、データが送られてくるたびに_transform()が実行され、内部のバッファに書き換えたデータをpushします。

次にfsモジュールのcreateReadStream()を使って、target.txtを読み込むRead streamを作成します。Read streamは一定量のデータを読み込んだら、dataイベントを発行します。今回登録しているリスナーでは、ToUpperクラスのインスタンスに読み込んだデータを渡す処理をしています。

こちら実行すると、chunkごとに改行で区切られ、大文字に変換されたテキストデータが出力されます。(サイズが大きいためこちらには一部のみ記載します)

SUSPENDISSE VITAE FRINGILLA LECTUS. FUSCE VENENATIS ENIM MAURIS, AC VIVERRA ENIM PORTA ID. SUSPENDISSE AUCTOR LECTUS EU ORCI SCELERISQUE SODALES. MORBI VESTIBULUM BLANDIT EGESTAS. NUNC EGET URNA IN JUSTO ORNARE FAUCIBUS. SUSPENDISSE ELEMENTUM LOREM NON TORTOR DIGNISSIM, SED EFFICITUR LOREM VOLUTPAT. QUISQUE LAOREET VESTIBULUM EST NON DAPIBUS. VESTIBULUM SIT AMET MAGNA EGET LACUS MOLLIS PELLENTESQUE.
DUIS MOLLIS LIGULA MI, AC SCELERISQUE DUI DAPIBUS EU. PRAESENT AC VENENATIS LECTUS, VITAE RHONCUS ELIT. NUNC SAGITTIS TURPIS QUIS DUI LOBORTIS, SED SOLLICITUDIN ERAT VEHIC



ULA. ETIAM A VENENATIS IPSUM, UT ELEMENTUM MAURIS. AENEAN ELEIFEND GRAVIDA VENENATIS. DONEC LOBORTIS NON SAPIEN AC TEMPOR. ALIQUAM TRISTIQUE JUSTO LIGULA, VITAE GRAVIDA ODIO MOLESTIE VEL. PRAESENT TRISTIQUE LECTUS EST, IN ORNARE ODIO FACILISIS UT. DONEC A EGESTAS QUAM. INTEGER TEMPUS SCELERISQUE ELIT, ET CONSEQUAT LACUS TEMPUS MATTIS.
LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING

なお、Stream同士の接続はpipe()を使うことで、もう少しスマートに同じことができます。

// 〜略〜
// Read streamを使って対象ファイルを読み込み
const rs = fs.createReadStream('target.txt');
// 一定量読み込み、次の処理にchunkを渡す
rs.pipe(toUpper);

最後に

あまりEventEmitterを直接いじる、ということはないと思うのですが、
普段何気なく使っているNodeモジュールやパッケージの内部ではこういった処理が使われていたんだな、と知ることができました。

参考書籍

Mario Casciaro、Luciano Mammino 著、武舎 広幸、阿部 和也 訳(2019)『Node.jsデザインパターン 第2版 』O'Reilly Japan, Inc.

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1