阿里云主机折上折
  • 微信号
Current Site:Index > Event emitter pattern

Event emitter pattern

Author:Chuan Chen 阅读数:25240人阅读 分类: Node.js

Event Emitter Pattern

The event emitter pattern is one of the core mechanisms for handling asynchronous events in Node.js. It allows objects to publish named events, and other objects can listen to these events and respond accordingly. This pattern decouples the relationship between event triggers and event handlers, making the code more modular and maintainable.

How Event Emitters Work

Node.js's events module provides the EventEmitter class, which is the foundation for implementing the event emitter pattern. Any object that inherits from EventEmitter can become an event emitter:

const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();

Event emitters primarily have two core methods:

  • emit(eventName[, ...args]): Triggers a specified event
  • on(eventName, listener): Adds a listener for a specified event

Basic Usage Example

Here is a simple example of using an event emitter:

const EventEmitter = require('events');

class Door extends EventEmitter {
  open() {
    console.log('Door opened');
    this.emit('open', new Date());
  }
  
  close() {
    console.log('Door closed');
    this.emit('close', new Date());
  }
}

const frontDoor = new Door();

// Add event listeners
frontDoor.on('open', (time) => {
  console.log(`Someone opened the door at ${time}`);
});

frontDoor.on('close', (time) => {
  console.log(`Someone closed the door at ${time}`);
});

// Trigger events
frontDoor.open();
setTimeout(() => frontDoor.close(), 1000);

Characteristics of Event Emitters

Synchronous Execution of Listeners

Event emitters call all listeners synchronously, executing them in the order they were registered:

const emitter = new EventEmitter();

emitter.on('event', () => console.log('First listener'));
emitter.on('event', () => console.log('Second listener'));

emitter.emit('event');
// Output:
// First listener
// Second listener

One-Time Listeners

The once() method can register a listener that triggers only once:

const emitter = new EventEmitter();

emitter.once('login', (user) => {
  console.log(`${user} logged in for the first time`);
});

emitter.emit('login', 'John'); // Will trigger
emitter.emit('login', 'John'); // Will not trigger

Error Event Handling

EventEmitter has special handling for error events. If no listener is registered for the error event, triggering it will cause Node.js to throw an exception and exit the process:

const emitter = new EventEmitter();

// Must handle error events
emitter.on('error', (err) => {
  console.error('Error occurred:', err.message);
});

emitter.emit('error', new Error('Something went wrong!'));

Advanced Usage

Getting Listener Information

const emitter = new EventEmitter();

const listener = () => console.log('Event triggered');
emitter.on('event', listener);

console.log(emitter.listenerCount('event')); // 1
console.log(emitter.eventNames()); // ['event']

Removing Listeners

const emitter = new EventEmitter();

function listener() {
  console.log('Event triggered');
  emitter.removeListener('event', listener);
}

emitter.on('event', listener);
emitter.emit('event'); // Will trigger
emitter.emit('event'); // Will not trigger

Setting Maximum Listener Count

By default, each event can have up to 10 listeners. Exceeding this number will output a warning:

const emitter = new EventEmitter();
emitter.setMaxListeners(20); // Increase to 20

Practical Use Cases

HTTP Server

Node.js's HTTP server is built on the event emitter pattern:

const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
  res.end('Hello World');
});

server.listen(3000);

Stream Processing

Node.js streams are also instances of event emitters:

const fs = require('fs');

const readStream = fs.createReadStream('file.txt');

readStream.on('data', (chunk) => {
  console.log(`Received ${chunk.length} bytes of data`);
});

readStream.on('end', () => {
  console.log('No more data');
});

Custom Event Bus

You can create a global event bus for communication between different modules:

// eventBus.js
const EventEmitter = require('events');
module.exports = new EventEmitter();

// moduleA.js
const bus = require('./eventBus');
bus.emit('data-ready', { data: 'some data' });

// moduleB.js
const bus = require('./eventBus');
bus.on('data-ready', (data) => {
  console.log('Received data:', data);
});

Performance Considerations

Avoiding Memory Leaks

Forgetting to remove listeners can cause memory leaks:

class Resource {
  constructor() {
    this.emitter = new EventEmitter();
    this.emitter.on('data', this.handleData);
  }
  
  handleData = (data) => {
    console.log(data);
  }
  
  destroy() {
    // Must remove listeners
    this.emitter.removeListener('data', this.handleData);
  }
}

Batch Operation Optimization

When multiple events need to be triggered, consider batch processing:

const emitter = new EventEmitter();

// Not recommended
for (let i = 0; i < 1000; i++) {
  emitter.emit('data', i);
}

// Recommended
const batch = [];
for (let i = 0; i < 1000; i++) {
  batch.push(i);
}
emitter.emit('batch-data', batch);

Comparison with Other Patterns

Comparison with Callback Pattern

The event emitter pattern is more flexible than the simple callback pattern:

// Callback pattern
function fetchData(callback) {
  // ...Fetch data
  callback(data);
}

// Event emitter pattern
class DataFetcher extends EventEmitter {
  fetch() {
    // ...Fetch data
    this.emit('data', data);
    this.emit('end');
  }
}

Comparison with Promise/async-await

Event emitters are suitable for handling multiple discrete events, while Promises are better for single asynchronous operations:

// Promise - Single operation
function getUser(id) {
  return new Promise((resolve, reject) => {
    // ...Get user
    resolve(user);
  });
}

// Event emitter - Continuous events
class UserTracker extends EventEmitter {
  track(userId) {
    setInterval(() => {
      const status = checkUserStatus(userId);
      this.emit('status-change', status);
    }, 1000);
  }
}

Extending EventEmitter

You can extend EventEmitter to add functionality:

class EnhancedEmitter extends EventEmitter {
  emitWithLog(event, ...args) {
    console.log(`Triggering event: ${event}`);
    return this.emit(event, ...args);
  }
  
  waitFor(event, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error('Event wait timeout'));
      }, timeout);
      
      this.once(event, (...args) => {
        clearTimeout(timer);
        resolve(args);
      });
    });
  }
}

// Usage example
const emitter = new EnhancedEmitter();
emitter.emitWithLog('test', 1, 2, 3);

(async () => {
  try {
    const result = await emitter.waitFor('data');
    console.log(result);
  } catch (err) {
    console.error(err);
  }
})();

Event Emitters in the Browser

Although Node.js's events module is server-side, similar concepts exist in browsers:

// Custom event emitter class
class BrowserEventEmitter {
  constructor() {
    this.listeners = {};
  }
  
  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }
  
  emit(event, ...args) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(cb => cb(...args));
    }
  }
}

// Usage example
const emitter = new BrowserEventEmitter();
emitter.on('click', (x, y) => {
  console.log(`Click position: ${x}, ${y}`);
});

emitter.emit('click', 100, 200);

Testing Event Emitters

When testing event emitters, you can use mocks or spy functions:

const assert = require('assert');
const EventEmitter = require('events');

describe('EventEmitter Tests', () => {
  it('should trigger event and call listener', () => {
    const emitter = new EventEmitter();
    let called = false;
    
    emitter.on('test', () => {
      called = true;
    });
    
    emitter.emit('test');
    assert.strictEqual(called, true);
  });
  
  it('should pass correct arguments', (done) => {
    const emitter = new EventEmitter();
    
    emitter.on('data', (a, b) => {
      try {
        assert.strictEqual(a, 1);
        assert.strictEqual(b, 2);
        done();
      } catch (err) {
        done(err);
      }
    });
    
    emitter.emit('data', 1, 2);
  });
});

Common Issues and Solutions

Too Many Event Listeners

When there are too many event listeners, consider using event aggregation:

const emitter = new EventEmitter();

// Original approach - Multiple independent events
emitter.on('user-added', handleUserAdded);
emitter.on('user-updated', handleUserUpdated);

// Improved approach - Aggregated events
emitter.on('user-change', (event) => {
  switch (event.type) {
    case 'added':
      handleUserAdded(event.user);
      break;
    case 'updated':
      handleUserUpdated(event.user);
      break;
  }
});

Event Order Dependencies

When events have order dependencies, you can use asynchronous control flow libraries:

const { AsyncSeriesWaterfallHook } = require('tapable');

const hook = new AsyncSeriesWaterfallHook(['data']);

hook.tapAsync('Step 1', (data, callback) => {
  setTimeout(() => {
    data.step1 = 'Done';
    callback(null, data);
  }, 100);
});

hook.tapAsync('Step 2', (data, callback) => {
  setTimeout(() => {
    data.step2 = 'Done';
    callback(null, data);
  }, 50);
});

hook.callAsync({}, (err, result) => {
  console.log(result);
});

Cross-Process Events

For cross-process event communication, you can use message queues or specialized libraries:

// Using Redis for cross-process event bus
const redis = require('redis');
const subscriber = redis.createClient();
const publisher = redis.createClient();

subscriber.on('message', (channel, message) => {
  console.log(`Received message: ${message} from channel: ${channel}`);
});

subscriber.subscribe('notifications');

// In another process
publisher.publish('notifications', 'Hello from Process B');

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.