「イベントシステム:EventEmitter—パブリッシュ/サブスクライブとイベント駆動型プログラミング」
アリスは、Eコマースの注文システムを開発しているバックエンドエンジニアです。当初、彼女は注文作成関数から、メール送信、在庫更新、ログ記録の3つのモジュールを直接呼び出していました。しかし、この方式では新機能を追加するたびに注文システムのコアコードを修正しなければならず、システムの脆弱性が高まっていました。その後、彼女はEventEmitterを使用してコードをリファクタリングしました。現在、注文モジュールはorder:createdイベントをトリガーすることのみを担当し、各サービスは互いに干渉することなく独立してイベントをリッスンしています。「SMS通知」を追加する場合も、リスナーを追加するだけで済み、コアコードへの変更は一切必要ありません。このパブリッシュ・サブスクライブ型アーキテクチャにより、システムの保守コストは60%削減されました。
1. 学習内容
- イベントエミッターの作成と、
EventEmitterクラスを用いたその拡張 on()/emit()/off()/once()/removeAllListeners()を使用してイベントを管理しますlistenerCount()/setMaxListeners()を使用して、リスナーの数やメモリリークの警告を管理します- カスタムイベントクラスを作成し、ビジネスロジックをカプセル化する
errorイベントおよび未捕捉の例外の適切な処理- Node.jsの組み込みモジュールにおける
EventEmitterの使用方法について - パブリッシュ・サブスクライブパターンを用いた、疎結合でイベント駆動型のアーキテクチャの構築
2. EventEmitterの基本概念
EventEmitter は、Node.js のイベント駆動型アーキテクチャの基盤となるクラスであり、events モジュール内にあります。このクラスは、イベント名とリスナー関数の配列との対応関係を管理しています。emit() がイベントをトリガーすると、登録されているすべてのリスナーが、登録された順序で 同期的に 実行されます。
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
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
emitter.emit('greet', 'Alice');
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 の基本的な使い方
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);
[Listener] Order created: #1001
Has listeners: false
4. on、once、prependListener の比較
| 特徴 | on() |
once() |
prependListener() |
|---|---|---|---|
| トリガーの数 | 毎回トリガーされる | 1回だけトリガーされ、その後自動的に削除される | 毎回トリガーされる |
| 登録場所 | 待ち行列の末尾 | 待ち行列の末尾 | 待ち行列の先頭(最初に実行される) |
| 代表的なシナリオ | メッセージの継続的な監視 | 初期化は1回のみ | 高優先度の処理(例:ロギング) |
| 自動削除 | いいえ | はい | いいえ |
| 使い捨てタイプに対応 | — | once() |
prependOnceListener() |
▶ 例:「once」は1回だけトリガーされます
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');
Server initialized on port 3000
Second emit had no effect
▶ 例:prependListener が最初に実行される
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');
Priority listener
Normal listener
5. イベントリスナーの管理
デフォルトでは、Node.js では 1 つのイベントにつき最大 10 個のリスナーが許可されています。この制限を超えると、メモリリークの警告が表示されます。これは厳格な制限というよりは、開発者がリスナーの削除を忘れていないか確認するよう促すための注意喚起です。
▶ 例:listenerCount とメモリリークの警告
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());
(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 による上限値の調整
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'));
No warning: max set to 20
Listener count: 15
▶ 例:removeAllListeners によるクリーンアップ
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'));
tick listeners: 0
tock listeners: 1
6. カスタムイベントクラス
実際の開発では、EventEmitterのインスタンスを直接使用するのではなく、EventEmitterを継承してビジネスセマンティクスを持つイベントクラスを作成するのが一般的です。
▶ 例:EventEmitter を継承して注文イベントクラスを作成する
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);
[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
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.emit('error', new Error('Database connection failed'));
events.js:291
throw er; // Unhandled 'error' event
^
Error: Database connection failed
at Object.<anonymous> (app.js:4:20)
▶ 例:error イベントの正しい処理方法
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');
[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
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');
});
Server listening on port 3000
[Connection] New client from ::ffff:127.0.0.1
[Request] GET /
▶ 例:EventEmitter を使用した読み取り可能なストリーム
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');
});
[Data] Received: "Hello"
[Data] Received: " "
[Data] Received: "World"
[End] No more data
10. 包括的な例:イベント駆動型タスクスケジューラ
タスクの登録、イベントのトリガー、および複数のリスナーからの応答をサポートする TaskScheduler クラスを作成してください。実際のシナリオにおいて、タスクが作成された後に発生する通知チェーンをシミュレートしてください。
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);
=== 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
❓ よくある質問
error イベントのリスナー登録を忘れた場合、どうなりますか?emit('error') が呼び出された時点で error のリスナーが登録されていない場合、Node.js はそのエラーを未処理の例外としてスローし、プロセスは直ちに終了します(process.on('uncaughtException') が設定されている場合を除く)。emitter.setMaxListeners(n)で調整可能で、0に設定すると上限が解除されます。emit() の戻り値は何ですか?true を返し、リスナーが存在しない場合は false を返します。これにより、イベントが処理されたかどうかを判断できます。once() メソッドを使用してリスナーを登録してください。これにより、1回トリガーされた後に自動的に削除されます。これは、初期化や1回限りのシグナルなどのシナリオに適しています。off() と removeListener() の違いは何ですか?off() は Node.js v10 removeListener() で導入された新しいエイリアスで、より簡潔な構文を提供します。新しいコードでは off() を使用することをお勧めします。this は何を指しますか?this は外側のスコープを継承します。通常の関数を使用する場合、this は EventEmitter インスタンスを指しますが、bind() を通じて別のオブジェクトにバインドされている場合は除きます。📖 まとめ
- 重要な概念とその活用方法
- EventEmitterの基本概念と使い方
- EventEmitterの基本概念と使用方法:よく使われるメソッドのクイックリファレンス
- 「on」、「once」、「prependListener」の主要な概念と使用法:比較
- イベントリスナーの基本概念と使い方
- カスタムイベントクラスの基本概念と使用方法
errorイベントおよびエラー処理の基本概念と使用方法- イベント駆動型、コールバック、プロミスの主要な概念と使い方:比較
📝 練習問題
- このレッスンにあるすべてのコード例を完成させ、それぞれが正しく動作することを確認してください。
- 包括的な例を修正し、独自の拡張機能を追加する
- 公式ドキュメントを確認し、このレッスンで扱われていないAPIを1~2つ見つけ、それらのテストコードを作成してください。
- 振り返り:このレッスンで学んだことを、実際のプロジェクトにどのように活かしますか?
- このレッスンで学んだことと、これまでのレッスンの内容を組み合わせて、小さなプロジェクトを作成してみてください。



