Event emitter pattern
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 eventon(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
上一篇:回调地狱问题与解决方案
下一篇:发布/订阅模式