デザインについての学習メモブログ

Node.js入門(3):イベント駆動プログラミング

記事内に広告が含まれています。

Node.js入門(3):イベント駆動プログラミング

Node.jsの最大の特徴の一つがイベント駆動アーキテクチャです。

この仕組みを理解することで、非同期処理を効率的に扱い、スケーラブルなアプリケーションを構築できるようになります。

💡この記事でわかる事

以下の内容を習得することができます:

  1. イベント駆動とは何か
  2. EventEmitterの基本
  3. 実践:カスタムイベントの作成
  4. イベントの高度な使い方
  5. よくある落とし穴とベストプラクティス
  6. 簡単なリアルタイムチャットシステムの作り方

イベント駆動とは何か

イベント駆動の基本概念

イベント駆動プログラミングとは、何かが起きたとき(イベント)に特定の処理(リスナー)を実行するプログラミングパラダイムです。

JavaScript
// 従来の同期的なアプローチ
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を基盤としています。

JavaScript
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の使い方

JavaScript
const EventEmitter = require('events');

// EventEmitterのインスタンスを作成
const emitter = new EventEmitter();

// イベントリスナーを登録
emitter.on('greeting', (name) => {
  console.log(`こんにちは、${name}さん!`);
});

// イベントを発火
emitter.emit('greeting', '太郎');// => こんにちは、太郎さん!

複数のリスナーを登録

1つのイベントに対して、複数の処理(リスナー)を登録できます。登録された順番に実行されます。

JavaScript
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()を使うと、イベントが最初に発火したときだけリスナーが実行され、自動的に削除されます。初期化処理などに便利です。

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

// once() は最初の1回だけ実行される
emitter.once('startup', () => {
  console.log('アプリケーション起動処理');
});

emitter.emit('startup');// => アプリケーション起動処理
emitter.emit('startup');// => 何も起こらない
emitter.emit('startup');// => 何も起こらない

リスナーの削除

不要になったリスナーは削除できます。メモリリークを防ぐためにも重要な操作です。

JavaScript
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を継承してカスタムクラスを作ります。

JavaScript
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']);

リアルタイムデータ処理の例

JavaScript
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はエラーをスローしてプロセスを終了させます。

JavaScript
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 で優先度を制御

JavaScript
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番目に実行

イベント名を動的に扱う

JavaScript
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' });

非同期イベントハンドラ

JavaScript
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');
})();

よくある落とし穴とベストプラクティス

メモリリークの防止

リスナーを削除し忘れると、メモリリークの原因になります。

JavaScript
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個以上のリスナーを登録すると警告が出ます。

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

// 上限を変更
emitter.setMaxListeners(20);

// または無制限にする(推奨されない)
// emitter.setMaxListeners(0);

// 現在の上限を確認
console.log(emitter.getMaxListeners());// => 20

同期 vs 非同期の選択

イベントハンドラは原則として同期的に実行されます。重い処理は非同期にしましょう。

JavaScript
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);
});

エラーハンドリングのベストプラクティス

JavaScript
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' });

実践例:リアルタイムチャットシステム

これまでの知識を使って、簡単なチャットシステムを作ってみましょう。

JavaScript
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回では、ストリーム処理について解説します。大きなファイルやリアルタイムデータを効率的に扱う方法を学びましょう!