Node.jsの多くのコアAPIは、イベント駆動アーキテクチャを基盤として構築されています。ファイルストリーム、HTTPサーバー、ソケット通信など、あらゆる場面でイベントが活用されています。この仕組みの中心にあるのがEventEmitterクラスです。
本記事では、EventEmitterの基本的な使い方からカスタムイベントクラスの作成まで、イベント駆動プログラミングの基礎を体系的に解説します。疎結合なアプリケーション設計を実現するための第一歩として、ぜひ習得してください。
実行環境#
| 項目 |
バージョン |
| Node.js |
20.x LTS以上 |
| npm |
10.x以上 |
| OS |
Windows/macOS/Linux |
前提条件#
- JavaScriptの基礎知識(クラス、コールバック関数)
- Node.jsの基本操作経験
- 非同期処理の基礎的な理解
EventEmitterとは#
EventEmitterは、Node.jsのnode:eventsモジュールで提供されるクラスで、イベントの発行(emit)と購読(subscribe)を実現します。オブジェクトが特定のイベントを発火すると、そのイベントに登録されたリスナー関数が順次呼び出されます。
sequenceDiagram
participant Publisher as イベント発行者
participant EventEmitter as EventEmitter
participant Listener1 as リスナー1
participant Listener2 as リスナー2
Listener1->>EventEmitter: on('event', callback1)
Listener2->>EventEmitter: on('event', callback2)
Publisher->>EventEmitter: emit('event', data)
EventEmitter->>Listener1: callback1(data)
EventEmitter->>Listener2: callback2(data)なぜイベント駆動が重要なのか#
イベント駆動プログラミングには以下のメリットがあります。
- 疎結合: イベント発行者とリスナーが直接依存しない
- 拡張性: 新しいリスナーを追加するだけで機能を拡張できる
- 非同期対応: Node.jsの非同期処理モデルと自然に統合できる
- 関心の分離: 各コンポーネントが自身の責務に集中できる
EventEmitterの基本操作#
EventEmitterのインポートとインスタンス化#
EventEmitterはnode:eventsモジュールからインポートします。
1
2
3
4
5
6
7
8
|
// CommonJS
const EventEmitter = require('node:events');
// ES Modules
import { EventEmitter } from 'node:events';
// インスタンス化
const emitter = new EventEmitter();
|
クラスを継承して独自のイベント発行オブジェクトを作成するのが一般的なパターンです。
1
2
3
4
5
|
const EventEmitter = require('node:events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
|
on() - イベントリスナーの登録#
on()メソッドは、指定したイベント名にリスナー関数を登録します。イベントが発火するたびに、登録されたリスナーが呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// 'greet'イベントにリスナーを登録
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
// イベント発火
emitter.emit('greet', 'Alice'); // Hello, Alice!
emitter.emit('greet', 'Bob'); // Hello, Bob!
|
同じイベントに複数のリスナーを登録することも可能です。リスナーは登録順に呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('data', () => console.log('First listener'));
emitter.on('data', () => console.log('Second listener'));
emitter.on('data', () => console.log('Third listener'));
emitter.emit('data');
// First listener
// Second listener
// Third listener
|
emit() - イベントの発火#
emit()メソッドは、指定したイベント名のイベントを発火します。第2引数以降に渡した値がリスナー関数の引数として渡されます。
1
2
3
4
5
6
7
8
9
10
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('message', (sender, content, timestamp) => {
console.log(`[${timestamp}] ${sender}: ${content}`);
});
emitter.emit('message', 'Alice', 'Hello!', new Date().toISOString());
// [2026-01-06T10:00:00.000Z] Alice: Hello!
|
emit()の戻り値は、イベントにリスナーが登録されていればtrue、なければfalseを返します。
1
2
3
4
5
6
7
8
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
console.log(emitter.emit('noListener')); // false
emitter.on('hasListener', () => {});
console.log(emitter.emit('hasListener')); // true
|
once() - 一度だけ実行されるリスナー#
once()メソッドは、イベントが発火した際に一度だけ実行されるリスナーを登録します。実行後は自動的に解除されます。
1
2
3
4
5
6
7
8
9
10
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.once('connect', () => {
console.log('Connected! (This message appears only once)');
});
emitter.emit('connect'); // Connected! (This message appears only once)
emitter.emit('connect'); // 何も出力されない
|
初期化処理や接続確立の通知など、一度だけ実行すればよい処理に適しています。
off() / removeListener() - リスナーの解除#
off()メソッド(removeListener()のエイリアス)は、登録したリスナーを解除します。解除するには、登録時と同じ関数参照を渡す必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
const callback = () => {
console.log('Event fired!');
};
emitter.on('event', callback);
emitter.emit('event'); // Event fired!
// リスナーを解除
emitter.off('event', callback);
emitter.emit('event'); // 何も出力されない
|
アロー関数をインラインで登録した場合、参照を保持していないと解除できない点に注意してください。
1
2
3
4
5
6
7
8
9
10
11
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// この方法では解除できない
emitter.on('event', () => console.log('Cannot remove'));
// 解除したい場合は関数を変数に保持する
const handler = () => console.log('Can remove');
emitter.on('event', handler);
emitter.off('event', handler); // 解除可能
|
removeAllListeners() - 全リスナーの解除#
特定のイベントまたは全イベントのリスナーをまとめて解除します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event1', () => console.log('Event 1'));
emitter.on('event2', () => console.log('Event 2'));
emitter.on('event2', () => console.log('Event 2 - second'));
// 特定のイベントのリスナーをすべて解除
emitter.removeAllListeners('event2');
emitter.emit('event2'); // 何も出力されない
emitter.emit('event1'); // Event 1
// 全イベントのリスナーを解除
emitter.removeAllListeners();
|
他のモジュールが登録したリスナーを意図せず解除してしまう可能性があるため、使用には注意が必要です。
リスナー管理のユーティリティメソッド#
listeners() - 登録済みリスナーの取得#
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
function handler1() {}
function handler2() {}
emitter.on('event', handler1);
emitter.on('event', handler2);
const listeners = emitter.listeners('event');
console.log(listeners); // [ [Function: handler1], [Function: handler2] ]
|
listenerCount() - リスナー数の取得#
1
2
3
4
5
6
7
8
9
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => {});
emitter.on('event', () => {});
emitter.once('event', () => {});
console.log(emitter.listenerCount('event')); // 3
|
eventNames() - 登録されているイベント名の取得#
1
2
3
4
5
6
7
8
9
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('connect', () => {});
emitter.on('disconnect', () => {});
emitter.on('data', () => {});
console.log(emitter.eventNames()); // [ 'connect', 'disconnect', 'data' ]
|
リスナーの実行順序制御#
prependListener() - リスナーを先頭に追加#
通常、リスナーは登録順に実行されます。prependListener()を使用すると、リスナーをキューの先頭に追加できます。
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => console.log('First'));
emitter.on('event', () => console.log('Second'));
emitter.prependListener('event', () => console.log('Prepended'));
emitter.emit('event');
// Prepended
// First
// Second
|
prependOnceListener() - 一度だけ実行されるリスナーを先頭に追加#
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => console.log('Always'));
emitter.prependOnceListener('event', () => console.log('Once at front'));
emitter.emit('event');
// Once at front
// Always
emitter.emit('event');
// Always
|
thisキーワードとアロー関数#
通常の関数をリスナーとして使用した場合、thisはEventEmitterインスタンスを参照します。
1
2
3
4
5
6
7
8
9
10
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', function() {
console.log(this === emitter); // true
console.log(this.constructor.name); // EventEmitter
});
emitter.emit('event');
|
アロー関数を使用すると、thisは外部スコープを参照するため、EventEmitterインスタンスにはアクセスできません。
1
2
3
4
5
6
7
8
9
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => {
console.log(this); // {} (モジュールスコープ) または undefined (strict mode)
});
emitter.emit('event');
|
EventEmitterインスタンスにアクセスする必要がある場合は、通常の関数を使用してください。
同期実行と非同期実行#
EventEmitterのリスナーは同期的に実行されます。これは重要な特性であり、意図しないブロッキングを避けるために理解しておく必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => {
console.log('1: Listener start');
// 重い処理をシミュレート
let sum = 0;
for (let i = 0; i < 1e9; i++) sum += i;
console.log('2: Listener end');
});
console.log('3: Before emit');
emitter.emit('event');
console.log('4: After emit');
// 出力順序:
// 3: Before emit
// 1: Listener start
// 2: Listener end
// 4: After emit
|
リスナー内で非同期処理を行いたい場合は、setImmediate()やprocess.nextTick()を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', () => {
setImmediate(() => {
console.log('This runs asynchronously');
});
console.log('Listener executed');
});
console.log('Before emit');
emitter.emit('event');
console.log('After emit');
// 出力順序:
// Before emit
// Listener executed
// After emit
// This runs asynchronously
|
エラーイベントの扱い#
EventEmitterにはerrorイベントに対する特別な動作があります。errorイベントが発火したときにリスナーが登録されていない場合、エラーがスローされ、Node.jsプロセスがクラッシュします。
1
2
3
4
5
6
7
8
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// エラーリスナーなしでerrorイベントを発火
emitter.emit('error', new Error('Something went wrong!'));
// Error: Something went wrong!
// Node.jsプロセスがクラッシュ
|
必ずerrorイベントリスナーを登録してください。
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// エラーリスナーを登録
emitter.on('error', (err) => {
console.error('Error occurred:', err.message);
});
emitter.emit('error', new Error('Something went wrong!'));
// Error occurred: Something went wrong!
// プロセスは継続
|
events.errorMonitorを使用したエラー監視#
events.errorMonitorシンボルを使用すると、エラーを消費せずに監視できます。ロギングやメトリクス収集に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const { EventEmitter, errorMonitor } = require('node:events');
const emitter = new EventEmitter();
// エラーを監視(消費しない)
emitter.on(errorMonitor, (err) => {
console.log('Monitoring error:', err.message);
});
// 通常のエラーハンドラ
emitter.on('error', (err) => {
console.log('Handling error:', err.message);
});
emitter.emit('error', new Error('Test error'));
// Monitoring error: Test error
// Handling error: Test error
|
errorMonitorリスナーだけでは、エラーイベントは消費されません。通常のerrorリスナーがない場合、プロセスはクラッシュします。
非同期リスナーとcaptureRejections#
async関数をリスナーとして使用する場合、Promiseの拒否(rejection)がunhandledRejectionを引き起こす可能性があります。
1
2
3
4
5
6
7
8
9
10
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
emitter.on('event', async () => {
throw new Error('Async error');
});
emitter.emit('event');
// UnhandledPromiseRejectionWarning: Error: Async error
|
captureRejectionsオプションを使用すると、async関数内のエラーをerrorイベントとして捕捉できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter({ captureRejections: true });
emitter.on('event', async () => {
throw new Error('Async error');
});
emitter.on('error', (err) => {
console.error('Caught async error:', err.message);
});
emitter.emit('event');
// Caught async error: Async error
|
リスナー数の上限設定#
デフォルトでは、1つのイベントに対して10個以上のリスナーを登録すると警告が表示されます。これはメモリリークの検出に役立ちます。
1
2
3
4
5
6
7
8
9
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
for (let i = 0; i < 11; i++) {
emitter.on('event', () => {});
}
// MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
// 11 event listeners added to [EventEmitter].
|
上限を変更するにはsetMaxListeners()を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// このインスタンスの上限を変更
emitter.setMaxListeners(20);
// 現在の上限を取得
console.log(emitter.getMaxListeners()); // 20
// 無制限にする場合は0またはInfinity
emitter.setMaxListeners(0);
|
グローバルなデフォルト値を変更することも可能ですが、慎重に使用してください。
1
2
3
4
|
const EventEmitter = require('node:events');
// すべての新しいEventEmitterインスタンスに適用
EventEmitter.defaultMaxListeners = 15;
|
newListenerとremoveListenerイベント#
EventEmitterは、リスナーの追加・削除時に自動的にイベントを発火します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
const EventEmitter = require('node:events');
const emitter = new EventEmitter();
// リスナー追加時のフック
emitter.on('newListener', (eventName, listener) => {
console.log(`Adding listener for "${eventName}"`);
});
// リスナー削除時のフック
emitter.on('removeListener', (eventName, listener) => {
console.log(`Removing listener for "${eventName}"`);
});
const handler = () => console.log('Hello');
emitter.on('greet', handler);
// Adding listener for "greet"
emitter.off('greet', handler);
// Removing listener for "greet"
|
カスタムイベントクラスの作成#
実践的なアプリケーションでは、EventEmitterを継承してカスタムクラスを作成します。
基本的なカスタムクラス#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
const EventEmitter = require('node:events');
class Timer extends EventEmitter {
constructor(duration) {
super();
this.duration = duration;
this.remaining = duration;
this.timerId = null;
}
start() {
if (this.timerId) return;
this.emit('start', this.remaining);
this.timerId = setInterval(() => {
this.remaining -= 1;
this.emit('tick', this.remaining);
if (this.remaining <= 0) {
this.stop();
this.emit('complete');
}
}, 1000);
}
stop() {
if (this.timerId) {
clearInterval(this.timerId);
this.timerId = null;
this.emit('stop', this.remaining);
}
}
reset() {
this.stop();
this.remaining = this.duration;
this.emit('reset');
}
}
// 使用例
const timer = new Timer(5);
timer.on('start', (remaining) => {
console.log(`Timer started: ${remaining} seconds`);
});
timer.on('tick', (remaining) => {
console.log(`Remaining: ${remaining} seconds`);
});
timer.on('complete', () => {
console.log('Timer completed!');
});
timer.start();
|
型安全なイベント定義(TypeScript)#
TypeScriptを使用する場合、型安全なイベント定義が可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import { EventEmitter } from 'node:events';
interface UserServiceEvents {
userCreated: [user: { id: string; name: string }];
userDeleted: [userId: string];
error: [error: Error];
}
class UserService extends EventEmitter {
emit<K extends keyof UserServiceEvents>(
event: K,
...args: UserServiceEvents[K]
): boolean {
return super.emit(event, ...args);
}
on<K extends keyof UserServiceEvents>(
event: K,
listener: (...args: UserServiceEvents[K]) => void
): this {
return super.on(event, listener);
}
createUser(name: string) {
const user = { id: crypto.randomUUID(), name };
this.emit('userCreated', user);
return user;
}
deleteUser(userId: string) {
// 削除処理
this.emit('userDeleted', userId);
}
}
const userService = new UserService();
userService.on('userCreated', (user) => {
console.log(`User created: ${user.name} (${user.id})`);
});
userService.createUser('Alice');
|
実践的なユースケース#
ファイル監視システム#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
const EventEmitter = require('node:events');
const fs = require('node:fs');
const path = require('node:path');
class FileWatcher extends EventEmitter {
constructor(directory) {
super();
this.directory = directory;
this.watcher = null;
}
start() {
this.watcher = fs.watch(this.directory, (eventType, filename) => {
if (!filename) return;
const filePath = path.join(this.directory, filename);
fs.stat(filePath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
this.emit('deleted', filename);
} else {
this.emit('error', err);
}
return;
}
if (eventType === 'rename') {
this.emit('created', filename, stats);
} else if (eventType === 'change') {
this.emit('modified', filename, stats);
}
});
});
this.emit('watching', this.directory);
}
stop() {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
this.emit('stopped');
}
}
}
// 使用例
const watcher = new FileWatcher('./watched-directory');
watcher.on('watching', (dir) => console.log(`Watching: ${dir}`));
watcher.on('created', (file) => console.log(`Created: ${file}`));
watcher.on('modified', (file) => console.log(`Modified: ${file}`));
watcher.on('deleted', (file) => console.log(`Deleted: ${file}`));
watcher.on('error', (err) => console.error(`Error: ${err.message}`));
watcher.start();
|
疎結合なモジュール間通信#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
const EventEmitter = require('node:events');
// グローバルイベントバス
class EventBus extends EventEmitter {
static instance = null;
static getInstance() {
if (!EventBus.instance) {
EventBus.instance = new EventBus();
}
return EventBus.instance;
}
}
// 注文モジュール
class OrderService {
constructor() {
this.bus = EventBus.getInstance();
}
createOrder(order) {
console.log(`Order created: ${order.id}`);
this.bus.emit('order:created', order);
}
}
// 在庫モジュール
class InventoryService {
constructor() {
this.bus = EventBus.getInstance();
this.bus.on('order:created', this.handleOrderCreated.bind(this));
}
handleOrderCreated(order) {
console.log(`Inventory: Processing order ${order.id}`);
// 在庫を減らす処理
}
}
// 通知モジュール
class NotificationService {
constructor() {
this.bus = EventBus.getInstance();
this.bus.on('order:created', this.handleOrderCreated.bind(this));
}
handleOrderCreated(order) {
console.log(`Notification: Sending confirmation for order ${order.id}`);
// 通知を送信する処理
}
}
// 各サービスを初期化
const inventoryService = new InventoryService();
const notificationService = new NotificationService();
const orderService = new OrderService();
// 注文を作成
orderService.createOrder({ id: 'ORD-001', items: ['item1', 'item2'] });
// Order created: ORD-001
// Inventory: Processing order ORD-001
// Notification: Sending confirmation for order ORD-001
|
events.once()によるPromise化#
events.once()関数を使用すると、イベントをPromiseとして待機できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const { EventEmitter, once } = require('node:events');
async function main() {
const emitter = new EventEmitter();
// 1秒後にイベントを発火
setTimeout(() => {
emitter.emit('ready', 'Server is ready!');
}, 1000);
// イベントを待機
const [message] = await once(emitter, 'ready');
console.log(message); // Server is ready!
}
main();
|
AbortSignalを使用してタイムアウトを設定することもできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const { EventEmitter, once } = require('node:events');
async function waitWithTimeout() {
const emitter = new EventEmitter();
const ac = new AbortController();
// 3秒後にタイムアウト
setTimeout(() => ac.abort(), 3000);
try {
const [data] = await once(emitter, 'data', { signal: ac.signal });
console.log('Received:', data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Timeout: Event did not fire in time');
} else {
throw error;
}
}
}
waitWithTimeout();
|
events.on()によるAsyncIterator#
events.on()を使用すると、イベントをAsyncIteratorとして処理できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
const { EventEmitter, on } = require('node:events');
async function processEvents() {
const emitter = new EventEmitter();
// 別のタイミングでイベントを発火
setTimeout(() => emitter.emit('data', 'First'), 100);
setTimeout(() => emitter.emit('data', 'Second'), 200);
setTimeout(() => emitter.emit('data', 'Third'), 300);
setTimeout(() => emitter.emit('close'), 400);
for await (const [value] of on(emitter, 'data', { close: ['close'] })) {
console.log('Received:', value);
}
console.log('Done processing');
}
processEvents();
// Received: First
// Received: Second
// Received: Third
// Done processing
|
まとめ#
本記事では、Node.jsのEventEmitterクラスについて以下の内容を解説しました。
- 基本操作:
on(), emit(), once(), off()メソッドの使い方
- リスナー管理: 実行順序の制御、リスナー数の上限設定
- エラーハンドリング:
errorイベントの特別な扱いとcaptureRejections
- カスタムクラス: EventEmitterを継承した独自クラスの作成
- 実践パターン: 疎結合なモジュール設計とイベントバス
EventEmitterはNode.jsのコアAPIの基盤であり、多くのモジュールがこのパターンを採用しています。イベント駆動プログラミングをマスターすることで、拡張性と保守性の高いアプリケーションを設計できるようになります。
参考リンク#