Node.jsの最大の特徴の一つがイベント駆動アーキテクチャです。
この仕組みを理解することで、非同期処理を効率的に扱い、スケーラブルなアプリケーションを構築できるようになります。
💡この記事でわかる事
以下の内容を習得することができます:
- イベント駆動とは何か
- EventEmitterの基本
- 実践:カスタムイベントの作成
- イベントの高度な使い方
- よくある落とし穴とベストプラクティス
- 簡単なリアルタイムチャットシステムの作り方
イベント駆動とは何か
イベント駆動の基本概念
イベント駆動プログラミングとは、何かが起きたとき(イベント)に特定の処理(リスナー)を実行するプログラミングパラダイムです。
// 従来の同期的なアプローチ
function processData() {
const data = readFile();// ファイル読み込みが終わるまで待つ
transform(data); // その後変換処理
saveFile(data); // その後保存
}
// イベント駆動のアプローチ
emitter.on('fileRead', (data) => {
transform(data);
});
emitter.on('transformComplete', (data) => {
saveFile(data);
});
readFileAsync();// 非同期で実行、完了したらイベント発火Node.jsにおけるイベント駆動
Node.jsのコアAPIの多くは、EventEmitterを基盤としています。
const fs = require('fs');
const http = require('http');
// HTTP サーバーもEventEmitterを継承している
const server = http.createServer();
server.on('request', (req, res) => {
console.log('リクエストを受信しました');
});
server.on('connection', (socket) => {
console.log('クライアントが接続しました');
});
server.on('close', () => {
console.log('サーバーが停止しました');
});
// Streamもイベントを発火する
const readStream = fs.createReadStream('large-file.txt');
readStream.on('data', (chunk) => {
console.log(`${chunk.length}バイト読み込みました`);
});
readStream.on('end', () => {
console.log('ファイル読み込み完了');
});EventEmitterの基本
EventEmitterは、Node.jsのeventsモジュールが提供するクラスで、イベントベースの通信を実現します。「何かが起きたら教えて」という仕組みを簡単に実装できます。
EventEmitterの使い方
const EventEmitter = require('events');
// EventEmitterのインスタンスを作成
const emitter = new EventEmitter();
// イベントリスナーを登録
emitter.on('greeting', (name) => {
console.log(`こんにちは、${name}さん!`);
});
// イベントを発火
emitter.emit('greeting', '太郎');// => こんにちは、太郎さん!複数のリスナーを登録
1つのイベントに対して、複数の処理(リスナー)を登録できます。登録された順番に実行されます。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 同じイベントに複数のリスナーを登録可能
emitter.on('userLogin', (username) => {
console.log(`ログ: ${username}がログインしました`);
});
emitter.on('userLogin', (username) => {
console.log(`通知を送信: ${username}さん、おかえりなさい`);
});
emitter.on('userLogin', (username) => {
console.log(`分析: ログインイベントを記録`);
});
emitter.emit('userLogin', 'alice');
// => ログ: aliceがログインしました
// => 通知を送信: aliceさん、おかえりなさい
// => 分析: ログインイベントを記録一度だけ実行するリスナー
once()を使うと、イベントが最初に発火したときだけリスナーが実行され、自動的に削除されます。初期化処理などに便利です。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// once() は最初の1回だけ実行される
emitter.once('startup', () => {
console.log('アプリケーション起動処理');
});
emitter.emit('startup');// => アプリケーション起動処理
emitter.emit('startup');// => 何も起こらない
emitter.emit('startup');// => 何も起こらないリスナーの削除
不要になったリスナーは削除できます。メモリリークを防ぐためにも重要な操作です。
const EventEmitter = require('events');
const emitter = new EventEmitter();
function onData(data) {
console.log('データ受信:', data);
}
// リスナーを登録
emitter.on('data', onData);
emitter.emit('data', 'test1'); // => データ受信: test1
// リスナーを削除
emitter.removeListener('data', onData);
// または emitter.off('data', onData);
emitter.emit('data', 'test2'); // => 何も起こらない
// すべてのリスナーを削除
emitter.removeAllListeners('data');実践:カスタムイベントの作成
EventEmitterを継承したクラス
実際のアプリケーションでは、EventEmitterを継承してカスタムクラスを作ります。
const EventEmitter = require('events');
class OrderProcessor extends EventEmitter {
constructor() {
super();
this.orders = [];
}
placeOrder(orderId, items) {
console.log(`注文 ${orderId} を受け付けました`);
// 注文受付イベントを発火
this.emit('orderPlaced', { orderId, items, timestamp: Date.now() });
// 処理をシミュレート
setTimeout(() => {
this.processOrder(orderId);
}, 1000);
}
processOrder(orderId) {
console.log(`注文 ${orderId} を処理中...`);
// 処理イベントを発火
this.emit('orderProcessing', { orderId });
setTimeout(() => {
this.completeOrder(orderId);
}, 2000);
}
completeOrder(orderId) {
console.log(`注文 ${orderId} が完了しました`);
// 完了イベントを発火
this.emit('orderCompleted', { orderId, timestamp: Date.now() });
}
}
// 使用例
const processor = new OrderProcessor();
// 各イベントにリスナーを登録
processor.on('orderPlaced', (order) => {
console.log(`📝 注文記録: ${order.orderId}`);
});
processor.on('orderProcessing', (order) => {
console.log(`⚙️ 処理開始: ${order.orderId}`);
});
processor.on('orderCompleted', (order) => {
console.log(`✅ 完了通知を送信: ${order.orderId}`);
});
// 注文を実行
processor.placeOrder('ORDER-001', ['商品A', '商品B']);リアルタイムデータ処理の例
const EventEmitter = require('events');
class DataStream extends EventEmitter {
constructor() {
super();
this.isActive = false;
}
start() {
if (this.isActive) {
this.emit('error', new Error('既にストリームは開始されています'));
return;
}
this.isActive = true;
this.emit('start');
// データを定期的に生成
this.interval = setInterval(() => {
const data = {
value: Math.random() * 100,
timestamp: Date.now()
};
this.emit('data', data);
// 閾値を超えたら警告
if (data.value > 90) {
this.emit('warning', { message: '値が高すぎます', data });
}
}, 1000);
}
stop() {
if (!this.isActive) return;
clearInterval(this.interval);
this.isActive = false;
this.emit('stop');
}
}
// 使用例
const stream = new DataStream();
stream.on('start', () => {
console.log('📊 データストリーム開始');
});
stream.on('data', (data) => {
console.log(`データ: ${data.value.toFixed(2)}`);
});
stream.on('warning', ({ message, data }) => {
console.warn(`⚠️ ${message}: ${data.value.toFixed(2)}`);
});
stream.on('stop', () => {
console.log('🛑 データストリーム停止');
});
stream.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
// 開始
stream.start();
// 10秒後に停止
setTimeout(() => {
stream.stop();
}, 10000);イベントの高度な使い方
エラーイベントの特別な扱い
errorイベントは特別で、リスナーが登録されていない場合、Node.jsはエラーをスローしてプロセスを終了させます。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 悪い例: errorリスナーがない
// emitter.emit('error', new Error('何か問題が発生'));
// => プロセスがクラッシュする!
// 良い例: errorリスナーを必ず登録
emitter.on('error', (err) => {
console.error('エラーが発生しました:', err.message);
// エラーを適切に処理
});
emitter.emit('error', new Error('何か問題が発生'));
// => エラーが発生しました: 何か問題が発生prependListener で優先度を制御
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('message', () => {
console.log('2番目に実行');
});
emitter.on('message', () => {
console.log('3番目に実行');
});
// prependListener で先頭に追加
emitter.prependListener('message', () => {
console.log('1番目に実行');
});
emitter.emit('message');
// => 1番目に実行
// => 2番目に実行
// => 3番目に実行イベント名を動的に扱う
const EventEmitter = require('events');
class NotificationCenter extends EventEmitter {
subscribe(userId, eventType) {
const eventName = `user:${userId}:${eventType}`;
this.on(eventName, (data) => {
console.log(`通知 [${userId}]: ${eventType} - ${JSON.stringify(data)}`);
});
}
notify(userId, eventType, data) {
const eventName = `user:${userId}:${eventType}`;
this.emit(eventName, data);
}
}
const center = new NotificationCenter();
// 特定ユーザーの特定イベントを購読
center.subscribe('user123', 'newMessage');
center.subscribe('user123', 'friendRequest');
center.subscribe('user456', 'newMessage');
// 通知を送信
center.notify('user123', 'newMessage', { from: 'Alice', text: 'こんにちは' });
center.notify('user456', 'newMessage', { from: 'Bob', text: 'やあ' });
center.notify('user123', 'friendRequest', { from: 'Charlie' });非同期イベントハンドラ
const EventEmitter = require('events');
class AsyncProcessor extends EventEmitter {
async processData(data) {
this.emit('start', data);
try {
// 非同期処理をシミュレート
await new Promise(resolve => setTimeout(resolve, 1000));
const result = data.toUpperCase();
this.emit('success', result);
return result;
} catch (err) {
this.emit('error', err);
throw err;
}
}
}
const processor = new AsyncProcessor();
processor.on('start', (data) => {
console.log('処理開始:', data);
});
processor.on('success', (result) => {
console.log('処理成功:', result);
});
processor.on('error', (err) => {
console.error('処理失敗:', err);
});
// 使用
(async () => {
await processor.processData('hello world');
})();よくある落とし穴とベストプラクティス
メモリリークの防止
リスナーを削除し忘れると、メモリリークの原因になります。
const EventEmitter = require('events');
class BadExample {
constructor(emitter) {
// 悪い例: リスナーを削除しない
emitter.on('data', (data) => {
this.process(data);
});
}
process(data) {
console.log(data);
}
}
class GoodExample {
constructor(emitter) {
this.emitter = emitter;
this.handler = (data) => this.process(data);
// リスナーを登録
this.emitter.on('data', this.handler);
}
process(data) {
console.log(data);
}
// クリーンアップメソッドを提供
destroy() {
this.emitter.removeListener('data', this.handler);
}
}
// 使用例
const emitter = new EventEmitter();
const instance = new GoodExample(emitter);
// 使い終わったら必ずクリーンアップ
instance.destroy();リスナー数の上限に注意
デフォルトでは、1つのイベントに10個以上のリスナーを登録すると警告が出ます。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 上限を変更
emitter.setMaxListeners(20);
// または無制限にする(推奨されない)
// emitter.setMaxListeners(0);
// 現在の上限を確認
console.log(emitter.getMaxListeners());// => 20同期 vs 非同期の選択
イベントハンドラは原則として同期的に実行されます。重い処理は非同期にしましょう。
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 悪い例: 重い同期処理がブロックする
emitter.on('process', (data) => {
// 重い計算処理...
for (let i = 0; i < 1000000000; i++) {
// ...
}
console.log('完了');
});
// 良い例: 非同期で処理
emitter.on('process', async (data) => {
// setImmediateやPromiseで非同期化
await new Promise(resolve => setImmediate(resolve));
// 重い処理
const result = await heavyComputation(data);
console.log('完了:', result);
});エラーハンドリングのベストプラクティス
const EventEmitter = require('events');
class RobustEmitter extends EventEmitter {
constructor() {
super();
// 常にerrorハンドラを登録
this.on('error', (err) => {
console.error('デフォルトエラーハンドラ:', err.message);
});
}
safeEmit(event, ...args) {
try {
this.emit(event, ...args);
} catch (err) {
this.emit('error', err);
}
}
}
const emitter = new RobustEmitter();
// カスタムエラーハンドラも追加可能
emitter.on('error', (err) => {
// ログサービスに送信
console.log('エラーをログに記録:', err);
});
// 安全に使用
emitter.safeEmit('someEvent', { data: 'test' });実践例:リアルタイムチャットシステム
これまでの知識を使って、簡単なチャットシステムを作ってみましょう。
const EventEmitter = require('events');
class ChatRoom extends EventEmitter {
constructor(name) {
super();
this.name = name;
this.users = new Map();
this.messageHistory = [];
}
join(userId, username) {
if (this.users.has(userId)) {
this.emit('error', new Error('ユーザーは既に参加しています'));
return;
}
this.users.set(userId, username);
this.emit('userJoined', { userId, username, userCount: this.users.size });
// 参加メッセージをブロードキャスト
this.broadcast('system', `${username}さんが参加しました`);
}
leave(userId) {
const username = this.users.get(userId);
if (!username) return;
this.users.delete(userId);
this.emit('userLeft', { userId, username, userCount: this.users.size });
this.broadcast('system', `${username}さんが退出しました`);
}
sendMessage(userId, text) {
const username = this.users.get(userId);
if (!username) {
this.emit('error', new Error('ユーザーが見つかりません'));
return;
}
const message = {
id: Date.now(),
userId,
username,
text,
timestamp: new Date().toISOString()
};
this.messageHistory.push(message);
this.emit('message', message);
}
broadcast(type, text) {
this.emit('broadcast', { type, text, timestamp: new Date().toISOString() });
}
getHistory(limit = 50) {
return this.messageHistory.slice(-limit);
}
}
// 使用例
const room = new ChatRoom('雑談部屋');
// イベントリスナーを設定
room.on('userJoined', ({ username, userCount }) => {
console.log(`👋 ${username}が参加 (現在${userCount}人)`);
});
room.on('userLeft', ({ username, userCount }) => {
console.log(`👋 ${username}が退出 (現在${userCount}人)`);
});
room.on('message', ({ username, text, timestamp }) => {
console.log(`💬 [${username}] ${text}`);
});
room.on('broadcast', ({ type, text }) => {
console.log(`📢 [${type}] ${text}`);
});
room.on('error', (err) => {
console.error('❌ エラー:', err.message);
});
// チャットをシミュレート
room.join('user1', 'Alice');
room.join('user2', 'Bob');
setTimeout(() => room.sendMessage('user1', 'こんにちは!'), 1000);
setTimeout(() => room.sendMessage('user2', 'やあ、Alice!'), 2000);
setTimeout(() => room.sendMessage('user1', '調子はどう?'), 3000);
setTimeout(() => room.leave('user2'), 5000);まとめ
イベント駆動プログラミングは、Node.jsの中核的な概念です。
重要なポイント:
- EventEmitterがNode.jsのイベントシステムの基盤
on()でリスナー登録、emit()でイベント発火- カスタムクラスにEventEmitterを継承させることで、柔軟な設計が可能
errorイベントは特別扱い – 必ずハンドラを登録する- メモリリークに注意 – 不要なリスナーは削除する
- 重い処理は非同期化してブロッキングを避ける
次回予告:
第3回では、ストリーム処理について解説します。大きなファイルやリアルタイムデータを効率的に扱う方法を学びましょう!


























