89. Observer (Наблюдатель)
Observer — поведенческий паттерн, в котором объект (Subject) ведёт список наблюдателей (Observers) и уведомляет их об изменениях. Это основа для систем событий, реактивного программирования и EventEmitter.
Базовая реализация
Заголовок раздела «Базовая реализация»class EventEmitter { #events = new Map()
on(event, listener) { if (!this.#events.has(event)) { this.#events.set(event, new Set()) } this.#events.get(event).add(listener) return () => this.off(event, listener) // возвращаем функцию отписки }
off(event, listener) { this.#events.get(event)?.delete(listener) return this }
emit(event, ...args) { this.#events.get(event)?.forEach(listener => listener(...args)) return this }
once(event, listener) { const wrapper = (...args) => { listener(...args) this.off(event, wrapper) } return this.on(event, wrapper) }}
// Использованиеconst emitter = new EventEmitter()
const unsubscribe = emitter.on('data', (value) => { console.log('Получено:', value)})
emitter.emit('data', 42) // 'Получено: 42'emitter.emit('data', 'hello') // 'Получено: hello'
unsubscribe() // отписываемся
emitter.emit('data', 'ещё') // ничего — подписка снятаObserver с типизированными событиями
Заголовок раздела «Observer с типизированными событиями»class Store extends EventEmitter { #state
constructor(initialState) { super() this.#state = initialState }
get state() { return { ...this.#state } }
setState(updates) { const prevState = this.#state this.#state = { ...this.#state, ...updates }
// Уведомляем о каждом изменённом поле for (const key in updates) { if (prevState[key] !== this.#state[key]) { this.emit(`change:${key}`, this.#state[key], prevState[key]) } } this.emit('change', this.#state, prevState) }}
const store = new Store({ count: 0, user: null, theme: 'dark' })
// Подписка на конкретное полеstore.on('change:count', (newVal, oldVal) => { console.log(`count: ${oldVal} → ${newVal}`)})
// Подписка на любое изменениеstore.on('change', (state) => { document.title = `Счёт: ${state.count}` // обновляем заголовок})
store.setState({ count: 1 }) // count: 0 → 1store.setState({ count: 5 }) // count: 1 → 5store.setState({ user: { name: 'Яша' } }) // только 'change' событиеSubject + Observer: классический GoF
Заголовок раздела «Subject + Observer: классический GoF»// Интерфейс наблюдателяclass Observer { update(data) { throw new Error('Не реализовано') }}
// Тема (Subject)class WeatherStation { #observers = new Set() #temperature = 0 #humidity = 0
subscribe(observer) { this.#observers.add(observer); return this } unsubscribe(observer) { this.#observers.delete(observer); return this }
#notify() { const data = { temperature: this.#temperature, humidity: this.#humidity } this.#observers.forEach(obs => obs.update(data)) }
setWeather(temp, humidity) { this.#temperature = temp this.#humidity = humidity this.#notify() }}
// Конкретные наблюдателиclass TemperatureDisplay extends Observer { update({ temperature }) { console.log(`🌡️ Температура: ${temperature}°C`) }}
class HumidityDisplay extends Observer { update({ humidity }) { console.log(`💧 Влажность: ${humidity}%`) }}
class AlertSystem extends Observer { update({ temperature, humidity }) { if (temperature > 35) console.log('🔥 ALERT: Высокая температура!') if (humidity > 80) console.log('💦 ALERT: Высокая влажность!') }}
const station = new WeatherStation()const tempDisplay = new TemperatureDisplay()const humidDisplay = new HumidityDisplay()const alerts = new AlertSystem()
station .subscribe(tempDisplay) .subscribe(humidDisplay) .subscribe(alerts)
station.setWeather(25, 60) // Обычная погодаstation.setWeather(37, 85) // ALERT!
station.unsubscribe(humidDisplay)station.setWeather(20, 50) // humidDisplay больше не получает обновленияРеактивное состояние с Proxy
Заголовок раздела «Реактивное состояние с Proxy»function createReactive(target, onChange) { return new Proxy(target, { set(obj, key, value) { const old = obj[key] obj[key] = value if (old !== value) onChange(key, value, old) return true } })}
const state = createReactive({ name: 'Яша', age: 1 }, (key, val, old) => { console.log(`${key}: ${old} → ${val}`) // обновляем DOM или что-то ещё})
state.name = 'Алексей' // name: Яша → Алексейstate.age = 2 // age: 1 → 2Когда использовать
Заголовок раздела «Когда использовать»✅ Observer подходит для:
- Системы событий / EventBus
- Реактивные UI фреймворки (React state, Vue reactive)
- WebSocket уведомления
- Живые дашборды и real-time данные
- MVC: Model уведомляет View