Let's build a powerful and flexible event emitter system in JavaScript, understanding the core concepts of event-driven programming. 🚀
Understanding Event Emitters 🎯
Event emitters implement the Observer pattern, allowing objects to communicate through custom events. They're fundamental to event-driven programming in JavaScript.
Basic Implementation
Core EventEmitter Class
class EventEmitter {
constructor() {
this.events = new Map();
}
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
return this;
}
emit(event, ...args) {
if (!this.events.has(event)) {
return false;
}
this.events.get(event).forEach(callback => {
callback.apply(this, args);
});
return true;
}
}
Basic Usage
const emitter = new EventEmitter();
emitter.on('userLogin', (user) => {
console.log(`${user.name} logged in`);
});
emitter.emit('userLogin', { name: 'John' });
Advanced Features 🌟
1. Once Listeners
class EventEmitter {
// ... previous methods ...
once(event, callback) {
const wrapper = (...args) => {
this.off(event, wrapper);
callback.apply(this, args);
};
wrapper.original = callback;
return this.on(event, wrapper);
}
}
// Usage
emitter.once('firstLogin', (user) => {
console.log(`Welcome ${user.name}!`);
});
2. Remove Listeners
class EventEmitter {
// ... previous methods ...
off(event, callback) {
if (!this.events.has(event)) {
return this;
}
if (!callback) {
this.events.delete(event);
return this;
}
const listeners = this.events.get(event);
const filtered = listeners.filter(listener => {
return listener !== callback &&
listener.original !== callback;
});
if (filtered.length === 0) {
this.events.delete(event);
} else {
this.events.set(event, filtered);
}
return this;
}
}
3. Error Handling
class EventEmitter {
// ... previous methods ...
emit(event, ...args) {
if (event === 'error' && !this.events.has('error')) {
throw args[0];
}
return super.emit(event, ...args);
}
}
// Usage
emitter.on('error', (error) => {
console.error('Caught error:', error);
});
Real-World Implementation 🔄
Complete EventEmitter
class EventEmitter {
constructor(options = {}) {
this.events = new Map();
this.maxListeners = options.maxListeners || 10;
}
addListener(event, listener) {
return this.on(event, listener);
}
on(event, listener) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
const listeners = this.events.get(event);
if (listeners.length >= this.maxListeners) {
console.warn(
`MaxListenersExceededWarning: Possible memory leak detected.
${listeners.length} ${event} listeners added.`
);
}
listeners.push(listener);
return this;
}
once(event, listener) {
const wrapper = (...args) => {
this.off(event, wrapper);
listener.apply(this, args);
};
wrapper.original = listener;
return this.on(event, wrapper);
}
off(event, listener) {
return this.removeListener(event, listener);
}
removeListener(event, listener) {
if (!this.events.has(event)) {
return this;
}
const listeners = this.events.get(event);
const filtered = listeners.filter(l => {
return l !== listener && l.original !== listener;
});
if (filtered.length === 0) {
this.events.delete(event);
} else {
this.events.set(event, filtered);
}
return this;
}
removeAllListeners(event) {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
return this;
}
listeners(event) {
return this.events.get(event) || [];
}
emit(event, ...args) {
if (!this.events.has(event)) {
if (event === 'error') {
throw args[0];
}
return false;
}
const listeners = this.events.get(event);
listeners.forEach(listener => {
try {
listener.apply(this, args);
} catch (error) {
this.emit('error', error);
}
});
return true;
}
listenerCount(event) {
return this.listeners(event).length;
}
}
Practical Examples 💡
1. Chat Application
class ChatRoom extends EventEmitter {
constructor() {
super();
this.users = new Set();
}
addUser(user) {
this.users.add(user);
this.emit('userJoined', user);
}
removeUser(user) {
this.users.delete(user);
this.emit('userLeft', user);
}
sendMessage(user, message) {
this.emit('message', { user, message });
}
}
const chat = new ChatRoom();
chat.on('userJoined', (user) => {
console.log(`${user.name} joined the chat`);
});
chat.on('message', ({ user, message }) => {
console.log(`${user.name}: ${message}`);
});
2. Data Store
class Store extends EventEmitter {
constructor(initialState = {}) {
super();
this.state = initialState;
}
setState(newState) {
const oldState = { ...this.state };
this.state = { ...this.state, ...newState };
this.emit('stateChanged', this.state, oldState);
}
getState() {
return this.state;
}
}
const store = new Store({ count: 0 });
store.on('stateChanged', (newState, oldState) => {
console.log('State changed:', { oldState, newState });
});
Performance Optimization 📊
1. Lazy Event Creation
class OptimizedEmitter extends EventEmitter {
on(event, listener) {
if (!this.events) {
this.events = new Map();
}
return super.on(event, listener);
}
}
2. Event Batching
class BatchEmitter extends EventEmitter {
constructor() {
super();
this.batchedEvents = new Map();
this.batchTimeout = null;
}
emitBatch(event, data) {
if (!this.batchedEvents.has(event)) {
this.batchedEvents.set(event, []);
}
this.batchedEvents.get(event).push(data);
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this.flush();
}, 0);
}
}
flush() {
this.batchedEvents.forEach((data, event) => {
this.emit(event, data);
});
this.batchedEvents.clear();
this.batchTimeout = null;
}
}
Best Practices 📝
- Always handle error events
- Clean up listeners when they're no longer needed
- Use meaningful event names
- Document event payloads
- Implement proper error handling
- Consider memory management
- Use TypeScript for better type safety
- Test event handling thoroughly
- Implement proper cleanup methods
- Monitor listener count
Additional Resources
Event emitters are a powerful tool for building event-driven applications. By implementing your own event emitter, you gain a deeper understanding of event-driven architecture and can customize the implementation to your specific needs.