Learning Objectives
- Understand the Observer pattern concept
- Implement publish-subscribe mechanisms
- Use observers for event systems
- Apply reactive programming patterns
- Build state management with observers
What is the Observer Pattern?
An Observer (subscriber) registers with a Subject (publisher) to receive notifications when the subject's state changes. It's a one-to-many relationship where one object notifies many dependents.
Basic Implementation
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} received:`, data);
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello observers!');
// Observer 1 received: Hello observers!
// Observer 2 received: Hello observers!
subject.unsubscribe(observer1);
subject.notify('Only one observer now');
// Observer 2 received: Only one observer now
Event Emitter Pattern
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
// Return unsubscribe function
return () => this.off(event, listener);
}
off(event, listener) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(l => l !== listener);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(listener => listener(data));
}
once(event, listener) {
const onceWrapper = (data) => {
listener(data);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
}
// Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on('data', (data) => {
console.log('Received:', data);
});
emitter.on('data', (data) => {
console.log('Also received:', data);
});
emitter.emit('data', { message: 'Hello' });
// Received: { message: 'Hello' }
// Also received: { message: 'Hello' }
// Use once for one-time listeners
emitter.once('init', () => {
console.log('Initialized once');
});
emitter.emit('init'); // Logs once
emitter.emit('init'); // Doesn't log
unsubscribe(); // Remove first listener
Real-World Examples
State Management Store
class Store {
constructor(initialState = {}) {
this.state = initialState;
this.listeners = [];
}
getState() {
return { ...this.state };
}
setState(newState) {
const prevState = this.state;
this.state = { ...this.state, ...newState };
this.notify(this.state, prevState);
}
subscribe(listener) {
this.listeners.push(listener);
// Return unsubscribe function
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notify(state, prevState) {
this.listeners.forEach(listener => listener(state, prevState));
}
}
// Usage
const store = new Store({ count: 0, user: null });
const unsubscribe = store.subscribe((state, prevState) => {
console.log('State changed from', prevState, 'to', state);
});
store.setState({ count: 1 });
// State changed from { count: 0, user: null } to { count: 1, user: null }
store.setState({ user: { name: 'John' } });
// State changed from { count: 1, user: null } to { count: 1, user: { name: 'John' } }
unsubscribe(); // Stop listening
DOM Observer
class DOMObserver {
constructor(element) {
this.element = element;
this.observers = [];
this.events = new Set();
}
observe(event) {
if (this.events.has(event)) return;
this.events.add(event);
this.element.addEventListener(event, (e) => {
this.notify(event, e);
});
}
subscribe(callback) {
this.observers.push(callback);
return () => {
this.observers = this.observers.filter(obs => obs !== callback);
};
}
notify(event, data) {
this.observers.forEach(observer => observer(event, data));
}
}
// Usage
const button = document.querySelector('#myButton');
const domObserver = new DOMObserver(button);
domObserver.observe('click');
domObserver.observe('mouseover');
const unsubscribe = domObserver.subscribe((event, data) => {
console.log(`Button ${event}:`, data);
});
News Publisher
class NewsPublisher {
constructor() {
this.subscribers = [];
this.articles = [];
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
console.log(`${subscriber.name} subscribed`);
}
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
console.log(`${subscriber.name} unsubscribed`);
}
publish(article) {
this.articles.push(article);
this.notifySubscribers(article);
}
notifySubscribers(article) {
this.subscribers.forEach(subscriber => {
subscriber.receive(article);
});
}
}
class Subscriber {
constructor(name) {
this.name = name;
this.articles = [];
}
receive(article) {
this.articles.push(article);
console.log(`${this.name} received: ${article.title}`);
}
}
// Usage
const publisher = new NewsPublisher();
const subscriber1 = new Subscriber('Alice');
const subscriber2 = new Subscriber('Bob');
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);
publisher.publish({
title: 'Breaking News',
content: 'Something important happened'
});
// Alice received: Breaking News
// Bob received: Breaking News
publisher.unsubscribe(subscriber1);
publisher.publish({
title: 'Update',
content: 'More news'
});
// Bob received: Update
When to Use Observer Pattern
Good use cases:
- Event systems and event handling
- State management (Redux, MobX)
- Real-time updates and notifications
- Reactive programming
- Pub-sub messaging systems
- Model-View updates (MVC)
Benefits
- Loose coupling: Subjects and observers are independent
- Dynamic relationships: Add/remove observers at runtime
- Broadcast communication: One-to-many notifications
- Easy to extend: Add new observers without changing subject
- Reactive: Automatic updates when state changes
Best Practices
1. Return unsubscribe function
subscribe(listener) {
this.listeners.push(listener);
return () => this.unsubscribe(listener);
}
const unsubscribe = store.subscribe(listener);
unsubscribe(); // Easy cleanup
2. Prevent memory leaks
class Component {
constructor() {
this.unsubscribers = [];
}
mount() {
const unsub = store.subscribe(this.handleChange);
this.unsubscribers.push(unsub);
}
unmount() {
this.unsubscribers.forEach(unsub => unsub());
this.unsubscribers = [];
}
}
3. Use weak references for DOM
class DOMObserver {
constructor() {
this.observers = new WeakMap();
}
}
Key Takeaways
- Observer pattern enables reactive systems
- Subject notifies observers of state changes
- Event emitters are a form of observer pattern
- Perfect for state management and event systems
- Foundation of reactive programming
- Promotes loose coupling
- Always provide unsubscribe mechanism
- Watch for memory leaks with DOM observers