Observer Pattern — Наблюдатель
Подписка на события объекта. В JS — встроенный
EventEmitter(Node) иEventTarget(browser). Источник развития: RxJS, Signals, реактивное программирование.
Проблема
Один объект меняется/генерирует события. Несколько других должны реагировать. Прямые вызовы создают зацепление. Хочется, чтобы источник не знал о потребителях.
Где используется
- DOM Events (
addEventListener— встроенный Observer) - EventEmitter в Node.js
- Реактивные библиотеки (RxJS, MobX)
- Redux store (subscribe)
- Vue.js reactivity, Signals (Svelte 5, Solid, Angular)
- Чаты, уведомления, real-time обновления
Решение
- Observable хранит список подписчиков и эмитит события.
- Observer подписывается через
subscribe(callback). - При событии — рассылка всем подписчикам.
Реализации
EventEmitter
class EventEmitter {
constructor { this.listeners = new Map(); }
on(event, callback) {
if (!this.listeners.has(event)) this.listeners.set(event, new Set);
this.listeners.get(event).add(callback);
return => this.off(event, callback); // unsubscribe
}
off(event, callback) { this.listeners.get(event)?.delete(callback); }
emit(event, ...args) {
this.listeners.get(event)?.forEach(cb => {
try { cb(...args); } catch (err) { console.error('listener error:', err); }
});
}
once(event, callback) {
const wrapper = (...args) => { callback(...args); this.off(event, wrapper); };
this.on(event, wrapper);
}
}
Через функции
const observable = () => {
const observers = new Set();
return {
subscribe: (fn) => { observers.add(fn); return => observers.delete(fn); },
emit: (value) => observers.forEach(o => o(value)),
};
};
Через встроенные API
// Node EventEmitter — синхронный
const { EventEmitter } = require('node:events');
const ee = new EventEmitter();
ee.on('data', chunk => process(chunk));
ee.emit('data', 'hello');
// DOM EventTarget — асинхронный через macrotask
const target = new EventTarget();
target.addEventListener('data', e => console.log(e.detail));
target.dispatchEvent(new CustomEvent('data', { detail: 'hello' }));
Store (как Redux)
class Store {
constructor(reducer, initial = {}) {
this.reducer = reducer;
this.state = initial;
this.listeners = new Set();
}
getState { return this.state; }
dispatch(action) {
const prev = this.state;
this.state = this.reducer(this.state, action);
if (prev !== this.state) this.listeners.forEach(cb => cb(this.state));
}
subscribe(cb) {
this.listeners.add(cb);
return => this.listeners.delete(cb);
}
}
Реактивные данные через Proxy
function createReactive(obj) {
const listeners = new Map();
const proxy = new Proxy(obj, {
set(target, prop, value) {
const old = target[prop];
target[prop] = value;
if (old !== value) listeners.get(prop)?.forEach(cb => cb(value, old));
return true;
}
});
proxy.$watch = (prop, cb) => {
if (!listeners.has(prop)) listeners.set(prop, new Set);
listeners.get(prop).add(cb);
return => listeners.get(prop).delete(cb);
};
return proxy;
}
const user = createReactive({ name: 'Иван' });
user.$watch('name', (v, old) => console.log(`${old} → ${v}`));
user.name = 'Пётр'; // Иван → Пётр
Где используется в JS-экосистеме
- EventEmitter (Node.js):
fs.createReadStream,http.Server— все унаследованы - EventTarget (DOM):
addEventListenerповсюду - RxJS Observable — стримы событий
- Vue/React reactivity — внутреннее использование observable
- Signals (Svelte 5, Solid.js, Angular Signals) — single-value observer
Подводные камни
- EventEmitter в Node — синхронный:
emitсразу обходит всех слушателей. - EventTarget — асинхронный через
dispatchEvent+ macrotask. onvsonce:onceотписывается после первого срабатывания.- Неконсистентность Node API:
EventEmitter.on(ee, 'data')(static) возвращает async iterator;ee.on(...)(instance) — подписку. - Зависимость от порядка подписчиков — архитектурный дефект, по автору.
- Сложно тестировать — две связанные событиями абстракции нельзя изолировать.
- Утечки памяти — забытые подписки накапливаются. Всегда сохраняй unsubscribe и вызывай в cleanup.
- Ошибка в подписчике ломает остальных без try/catch.
- Бесконечный цикл уведомлений — A подписан на B, B подписан на A.
Главные тезисы автора
- «Одна абстракция порождает события, другая наблюдает за ними».
- «Не нужно делать observable и observer как в GoF — listener это просто функция» (в JS).
- EventEmitter для backend (Node), EventTarget для frontend (DOM).
- Критика асинхронности через EventEmitter: «event-emitter подобен go-to».
- «События делают зацепление неявным» — кажется, что снизили coupling, на самом деле просто спрятали.
- «Если код на event-emitter — это тестировать сложнее» — нельзя изолировать.
- Signals = single-value Observer — современная вариация.
🎓 Источники
- 🎓 Паттерн Наблюдатель (Observer + Observable) · 2019-05-21
- Две абстракции Observer
- Реализация на функциях, на классах
- RxJS как развитие
- 🎓 Observer Pattern: EventEmitter, EventTarget · 2025-11-06
- EventEmitter (Node) vs EventTarget (web)
- «EventEmitter подобен go-to»
- События делают зацепление неявным
- 🎓 EventEmitter, Symbol, Proxy · 2020-01-05
- 🎓 GoF Patterns Обзор всех паттернов · 2025-04-29
- Refactoring Guru — Observer
- Patterns.dev — Observer