404 Not Found

404 Not Found


nginx

「イベントシステム:EventEmitter—パブリッシュ/サブスクライブとイベント駆動型プログラミング」

アリスは、Eコマースの注文システムを開発しているバックエンドエンジニアです。当初、彼女は注文作成関数から、メール送信、在庫更新、ログ記録の3つのモジュールを直接呼び出していました。しかし、この方式では新機能を追加するたびに注文システムのコアコードを修正しなければならず、システムの脆弱性が高まっていました。その後、彼女はEventEmitterを使用してコードをリファクタリングしました。現在、注文モジュールはorder:createdイベントをトリガーすることのみを担当し、各サービスは互いに干渉することなく独立してイベントをリッスンしています。「SMS通知」を追加する場合も、リスナーを追加するだけで済み、コアコードへの変更は一切必要ありません。このパブリッシュ・サブスクライブ型アーキテクチャにより、システムの保守コストは60%削減されました。

1. 学習内容


2. EventEmitterの基本概念

EventEmitter は、Node.js のイベント駆動型アーキテクチャの基盤となるクラスであり、events モジュール内にあります。このクラスは、イベント名とリスナー関数の配列との対応関係を管理しています。emit() がイベントをトリガーすると、登録されているすべてのリスナーが、登録された順序で 同期的に 実行されます。

100%
flowchart LR
    subgraph Emitter["EventEmitter(Posted by)"]
        E1[emit - order:created]
    end
    subgraph Listeners["Listener(Subscribers)"]
        L1[Listener1:Send an Email]
        L2[Listener2:Update Inventory]
        L3[Listener3:Log Entry]
    end
    E1 --> L1
    E1 --> L2
    E1 --> L3
    style Emitter fill:#e1f5fe
    style Listeners fill:#f3e5f5
JAVASCRIPT
const EventEmitter = require('events');

const emitter = new EventEmitter();

emitter.on('greet', (name) => {
  console.log(`Hello, ${name}!`);
});

emitter.emit('greet', 'Alice');
TEXT
Hello, Alice!

3. EventEmitter の一般的なメソッドのクイックリファレンス

メソッド 説明 戻り値
on(event, listener) リスナーを登録します。複数回登録可能です EventEmitter インスタンス
once(event, listener) トリガーされた際に自動的に削除されるワンタイムリスナーを登録する EventEmitter インスタンス
emit(event, ...args) イベントをトリガーし、パラメータを渡す true リスナーがある / false リスナーがない
off(event, listener) 指定したリスナーを削除する EventEmitter インスタンス
removeListener(event, listener) off() と同じ、旧API EventEmitter インスタンス
removeAllListeners([event]) すべてのイベントまたは指定したイベントのリスナーを削除する EventEmitter インスタンス
prependListener(event, listener) キューの先頭にリスナーを追加する EventEmitter インスタンス
prependOnceListener(event, listener) 先頭にワンタイムリスナーを追加 EventEmitter インスタンス
listeners(event) イベントリスナーの配列を返す Function[]
listenerCount(event) リスナー数 number
setMaxListeners(n) 単一のイベントに対するリスナーの最大数を設定する EventEmitter インスタンス
getMaxListeners() リスナーの最大数を取得する number
eventNames() 登録されているすべてのイベント名を返す `(string
rawListeners(event) once のタグが付いた元のリスナーを返す Function[]

▶ 例:on/emit/off の基本的な使い方

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

function onOrderCreated(order) {
  console.log(`[Listener] Order created: #${order.id}`);
}

emitter.on('order:created', onOrderCreated);

emitter.emit('order:created', { id: 1001, product: 'Laptop', qty: 2 });

emitter.off('order:created', onOrderCreated);

const hasListeners = emitter.emit('order:created', { id: 1002, product: 'Mouse', qty: 5 });
console.log('Has listeners:', hasListeners);
TEXT
[Listener] Order created: #1001
Has listeners: false

4. on、once、prependListener の比較

特徴 on() once() prependListener()
トリガーの数 毎回トリガーされる 1回だけトリガーされ、その後自動的に削除される 毎回トリガーされる
登録場所 待ち行列の末尾 待ち行列の末尾 待ち行列の先頭(最初に実行される)
代表的なシナリオ メッセージの継続的な監視 初期化は1回のみ 高優先度の処理(例:ロギング)
自動削除 いいえ はい いいえ
使い捨てタイプに対応 once() prependOnceListener()

▶ 例:「once」は1回だけトリガーされます

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.once('server:ready', (port) => {
  console.log(`Server initialized on port ${port}`);
});

emitter.emit('server:ready', 3000);
emitter.emit('server:ready', 3001);
console.log('Second emit had no effect');
TEXT
Server initialized on port 3000
Second emit had no effect

▶ 例:prependListener が最初に実行される

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('data', () => console.log('Normal listener'));
emitter.prependListener('data', () => console.log('Priority listener'));

emitter.emit('data');
TEXT
Priority listener
Normal listener

5. イベントリスナーの管理

デフォルトでは、Node.js では 1 つのイベントにつき最大 10 個のリスナーが許可されています。この制限を超えると、メモリリークの警告が表示されます。これは厳格な制限というよりは、開発者がリスナーの削除を忘れていないか確認するよう促すための注意喚起です。

▶ 例:listenerCount とメモリリークの警告

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

for (let i = 0; i < 12; i++) {
  emitter.on('log', () => console.log(`Listener ${i}`));
}

console.log('Listener count:', emitter.listenerCount('log'));
console.log('Max listeners:', emitter.getMaxListeners());
TEXT
(node:1234) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 12 log listeners added. Use emitter.setMaxListeners() to increase limit
Listener count: 12
Max listeners: 10

▶ 例:setMaxListeners による上限値の調整

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.setMaxListeners(20);

for (let i = 0; i < 15; i++) {
  emitter.on('task', () => {});
}

console.log('No warning: max set to', emitter.getMaxListeners());
console.log('Listener count:', emitter.listenerCount('task'));
TEXT
No warning: max set to 20
Listener count: 15

▶ 例:removeAllListeners によるクリーンアップ

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('tick', () => console.log('tick A'));
emitter.on('tick', () => console.log('tick B'));
emitter.on('tock', () => console.log('tock A'));

emitter.removeAllListeners('tick');

console.log('tick listeners:', emitter.listenerCount('tick'));
console.log('tock listeners:', emitter.listenerCount('tock'));
TEXT
tick listeners: 0
tock listeners: 1

6. カスタムイベントクラス

実際の開発では、EventEmitterのインスタンスを直接使用するのではなく、EventEmitterを継承してビジネスセマンティクスを持つイベントクラスを作成するのが一般的です。

▶ 例:EventEmitter を継承して注文イベントクラスを作成する

JAVASCRIPT
const EventEmitter = require('events');

class OrderEmitter extends EventEmitter {
  create(order) {
    this.emit('order:created', order);
  }

  cancel(orderId) {
    this.emit('order:cancelled', { orderId, cancelledAt: new Date().toISOString() });
  }

  ship(orderId, trackingNo) {
    this.emit('order:shipped', { orderId, trackingNo });
  }
}

const orderEvents = new OrderEmitter();

orderEvents.on('order:created', (order) => {
  console.log(`[Email] Confirmation for order #${order.id}`);
});

orderEvents.on('order:created', (order) => {
  console.log(`[Inventory] Deduct ${order.qty}x ${order.product}`);
});

orderEvents.on('order:cancelled', ({ orderId }) => {
  console.log(`[Refund] Processing refund for #${orderId}`);
});

orderEvents.create({ id: 2001, product: 'Headphones', qty: 3 });
orderEvents.cancel(2001);
TEXT
[Email] Confirmation for order #2001
[Inventory] Deduct 3x Headphones
[Refund] Processing refund for #2001

7. error イベントとエラー処理

EventEmitter has a special error event: if the error event is triggered but there are no listeners, Node.js will throw an exception and terminate the process. This is the most important safety rule in EventEmitter.

▶ Example: Process crash due to failure to handle an error

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.emit('error', new Error('Database connection failed'));
TEXT
events.js:291
      throw er; // Unhandled 'error' event
      ^
Error: Database connection failed
    at Object.<anonymous> (app.js:4:20)

▶ 例:error イベントの正しい処理方法

JAVASCRIPT
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('error', (err) => {
  console.error(`[Error Handler] ${err.message}`);
});

emitter.emit('error', new Error('Database connection failed'));
console.log('Process continues running');
TEXT
[Error Handler] Database connection failed
Process continues running

8. イベント駆動型、コールバック、およびプロミスの比較

機能 イベント駆動型 (EventEmitter) コールバック Promise / async-await
通信モード 1対多(パブリッシュ・サブスクライブ) 1対1 1対1
トリガーの数 複数回トリガー可能 1回のみトリガー 1回のみ解決
代表的なシナリオ メッセージのブロードキャスト、状態の変化 I/O 操作の完了 非同期操作の最終結果
結合度 低(パブリッシャーはサブスクライバーを知らない) 高(呼び出し元はコールバックを知っている必要がある) 中(チェーン化またはawait
キャンセル可能 off() で削除可能 キャンセル不可 キャンセル不可(無視可能)
組み込みサポート events モジュール グローバル規約 組み込み言語
エラー処理 error イベント エラー優先度コールバック .catch() / try-catch

9. EventEmitter を使用する Node.js の組み込みモジュール

Node.js の組み込みモジュールの多くは EventEmitter を継承しており、ストリーム、サーバー、プロセスのオブジェクトのほぼすべてがイベントエミッタとなっています。

モジュール/オブジェクト EventEmitter を継承 一般的なイベント
net.Server はい connection, close, error
http.Server はい request, connection, close
stream.Readable はい data, end, error, close
stream.Writable はい drain, finish, error, close
net.Socket はい data, connect, end, error
process はい exit, uncaughtException, SIGINT
child_process.ChildProcess はい exit, message, error, close
fs.watch() EventEmitter に戻る change, error

▶ 例:EventEmitter を使用した http.Server

JAVASCRIPT
const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
  console.log(`[Request] ${req.method} ${req.url}`);
  res.end('OK');
});

server.on('connection', (socket) => {
  console.log(`[Connection] New client from ${socket.remoteAddress}`);
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});
TEXT
Server listening on port 3000
[Connection] New client from ::ffff:127.0.0.1
[Request] GET /

▶ 例:EventEmitter を使用した読み取り可能なストリーム

JAVASCRIPT
const { Readable } = require('stream');

const readable = Readable.from(['Hello', ' ', 'World']);

readable.on('data', (chunk) => {
  console.log(`[Data] Received: "${chunk}"`);
});

readable.on('end', () => {
  console.log('[End] No more data');
});
TEXT
[Data] Received: "Hello"
[Data] Received: " "
[Data] Received: "World"
[End] No more data

10. 包括的な例:イベント駆動型タスクスケジューラ

タスクの登録、イベントのトリガー、および複数のリスナーからの応答をサポートする TaskScheduler クラスを作成してください。実際のシナリオにおいて、タスクが作成された後に発生する通知チェーンをシミュレートしてください。

JAVASCRIPT
const EventEmitter = require('events');

class TaskScheduler extends EventEmitter {
  constructor() {
    super();
    this.tasks = new Map();
    this.nextId = 1;
  }

  addTask(name, payload) {
    const id = this.nextId++;
    const task = {
      id,
      name,
      payload,
      createdAt: new Date().toISOString(),
      status: 'pending'
    };
    this.tasks.set(id, task);
    this.emit('task:added', task);
    return task;
  }

  startTask(id) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'running';
    task.startedAt = new Date().toISOString();
    this.emit('task:started', task);
  }

  completeTask(id, result) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'completed';
    task.result = result;
    task.completedAt = new Date().toISOString();
    this.emit('task:completed', task);
  }

  failTask(id, reason) {
    const task = this.tasks.get(id);
    if (!task) {
      this.emit('error', new Error(`Task #${id} not found`));
      return;
    }
    task.status = 'failed';
    task.reason = reason;
    task.failedAt = new Date().toISOString();
    this.emit('task:failed', task);
  }

  getStats() {
    const stats = { total: this.tasks.size, pending: 0, running: 0, completed: 0, failed: 0 };
    for (const task of this.tasks.values()) {
      stats[task.status]++;
    }
    return stats;
  }
}

const scheduler = new TaskScheduler();

scheduler.on('error', (err) => {
  console.error(`[ERROR] ${err.message}`);
});

scheduler.on('task:added', (task) => {
  console.log(`[Logger] Task #${task.id} "${task.name}" added at ${task.createdAt}`);
});

scheduler.on('task:added', (task) => {
  console.log(`[Notifier] New task available: ${task.name}`);
});

scheduler.on('task:started', (task) => {
  console.log(`[Executor] Task #${task.id} is now running...`);
});

scheduler.on('task:completed', (task) => {
  console.log(`[Reporter] Task #${task.id} completed with result: ${task.result}`);
  console.log(`[Cleaner] Releasing resources for task #${task.id}`);
});

scheduler.on('task:failed', (task) => {
  console.log(`[Alerter] Task #${task.id} failed: ${task.reason}`);
});

console.log('=== Adding Tasks ===');
const t1 = scheduler.addTask('Data Import', { source: 'api.example.com', rows: 5000 });
const t2 = scheduler.addTask('Report Generation', { format: 'PDF', quarter: 'Q4' });

console.log('\n=== Starting Tasks ===');
scheduler.startTask(t1.id);
scheduler.startTask(t2.id);

console.log('\n=== Completing / Failing Tasks ===');
scheduler.completeTask(t1.id, '5000 rows imported successfully');
scheduler.failTask(t2.id, 'PDF renderer service unavailable');

console.log('\n=== Stats ===');
console.log(scheduler.getStats());

console.log('\n=== Invalid Operation ===');
scheduler.startTask(999);
TEXT
=== Adding Tasks ===
[Logger] Task #1 "Data Import" added at 2025-07-03T10:00:00.000Z
[Notifier] New task available: Data Import
[Logger] Task #2 "Report Generation" added at 2025-07-03T10:00:01.000Z
[Notifier] New task available: Report Generation

=== Starting Tasks ===
[Executor] Task #1 is now running...
[Executor] Task #2 is now running...

=== Completing / Failing Tasks ===
[Reporter] Task #1 completed with result: 5000 rows imported successfully
[Cleaner] Releasing resources for task #1
[Alerter] Task #2 failed: PDF renderer service unavailable

=== Stats ===
{ total: 2, pending: 0, running: 0, completed: 1, failed: 1 }

=== Invalid Operation ===
[ERROR] Task #999 not found

❓ よくある質問

Q EventEmitterとブラウザのイベントの違いは何ですか?
A Node.jsのEventEmitterは同期的に実行され、リスナーは登録された順序で実行されます。一方、ブラウザのDOMイベントは非同期であり、キャプチャフェーズとバブリングフェーズを経由し、イベントオブジェクトの構造もNode.jsとは異なります。
Q error イベントのリスナー登録を忘れた場合、どうなりますか?
A emit('error') が呼び出された時点で error のリスナーが登録されていない場合、Node.js はそのエラーを未処理の例外としてスローし、プロセスは直ちに終了します(process.on('uncaughtException') が設定されている場合を除く)。
Q 1つのイベントが持つことができるリスナーの最大数はいくつですか?
A デフォルトの上限は10です。この上限を超えると、メモリリークの警告が表示されます(ただし、実行はブロックされません)。この設定はemitter.setMaxListeners(n)で調整可能で、0に設定すると上限が解除されます。
Q emit() の戻り値は何ですか?
A イベントに少なくとも 1 つのリスナーが存在する場合は true を返し、リスナーが存在しない場合は false を返します。これにより、イベントが処理されたかどうかを判断できます。
Q リスナーが1回だけトリガーされた後に自動的に削除されるようにするにはどうすればよいですか?
A once() メソッドを使用してリスナーを登録してください。これにより、1回トリガーされた後に自動的に削除されます。これは、初期化や1回限りのシグナルなどのシナリオに適しています。
Q off()removeListener() の違いは何ですか?
A 機能はまったく同じです。off() は Node.js v10 removeListener() で導入された新しいエイリアスで、より簡潔な構文を提供します。新しいコードでは off() を使用することをお勧めします。
Q リスナーにおける this は何を指しますか?
A ES6 の矢印関数を使用する場合、this は外側のスコープを継承します。通常の関数を使用する場合、thisEventEmitter インスタンスを指しますが、bind() を通じて別のオブジェクトにバインドされている場合は除きます。

📖 まとめ


📝 練習問題

  1. このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
  2. 包括的な例を修正し、独自の拡張機能を追加する
  3. 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
  4. 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
  5. このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%