Один из наиболее элегантных способов организации взаимодействия между компонентами — паттерн Observer. Он обеспечивает слабую связанность, но его неправильная реализация часто приводит к утечкам памяти и неочевидным багам. Разберёмся, как реализовать его корректно и где он применяется в современных веб-технологиях.
Проблема: Жёсткие связи и хрупкая коммуникация
Представьте email-рассылку: пользователи подписываются на новости, а система уведомляет их о новых статьях. Наивный подход — хранить массив подписчиков в классе Newsletter
и вызывать их методы напрямую:
class Newsletter {
constructor() {
this.subscribers = [];
}
addSubscriber(user) {
this.subscribers.push(user);
}
sendUpdate(article) {
this.subscribers.forEach(subscriber => {
subscriber.sendEmail(article); // Прямая зависимость!
});
}
}
Проблемы:
- Класс
Newsletter
жёстко зависим от интерфейсаsubscriber.sendEmail
. - Подписчики не могут контролировать тип уведомлений (SMS, push и т.д.).
- Удалить подписку сложно — требуется фильтрация массива.
Реализация Observer: Декомпозиция ответственности
Паттерн Observer разделяет сущности на:
- Subject (издатель): управляет подписчиками и оповещает их.
- Observer (подписчик): предоставляет интерфейс для реакции на события.
Шаг 1: Абстракция подписчика
class Subscriber {
update(article) {
throw new Error("Метод update должен быть переопределён");
}
}
Шаг 2: Рефакторинг издателя
class Newsletter {
constructor() {
this.subscribers = new Map(); // Используем Map для эффективного удаления
}
subscribe(observer) {
const id = Symbol(); // Уникальный идентификатор подписки
this.subscribers.set(id, observer);
return () => this.subscribers.delete(id); // Функция отмены подписки
}
sendUpdate(article) {
for (const observer of this.subscribers.values()) {
observer.update(article);
}
}
}
Шаг 3: Конкретные подписчики
class EmailSubscriber extends Subscriber {
constructor(email) {
super();
this.email = email;
}
update(article) {
console.log(`Отправлено на ${this.email}: "${article.title}"`);
}
}
class PushSubscriber extends Subscriber {
update(article) {
console.log(`Push-уведомление: Новая статья "${article.title}"`);
}
}
Использование:
const newsletter = new Newsletter();
const unsubscribeJohn = newsletter.subscribe(
new EmailSubscriber("john@example.com")
);
const unsubscribeApp = newsletter.subscribe(
new PushSubscriber()
);
// Отправка новости
newsletter.sendUpdate({ title: "JavaScript Patterns 2023" });
// Отмена подписки John
unsubscribeJohn();
Ключевые преимущества
- Слабая связанность: Издатель ничего не знает о деталях реализации подписчиков.
- Динамическое управление: Подписки добавляются/удаляются в рантайме.
- Масштабируемость: Новые типы подписчиков (SMS, логирование) не требуют изменений в
Newsletter
.
Распространённые ошибки и их решение
Проблема: Утечки памяти
Если забыть отписать объект, он останется в памяти (особенно критично для SPA).
Решение:
- Всегда возвращайте функцию отписки (как в примере выше).
- В современных UI-фреймворках используйте встроенные средства (например,
useEffect
возвращает cleanup-функцию):
useEffect(() => {
const unsubscribe = newsletter.subscribe(/* ... */);
return unsubscribe; // Отписка при размонтировании
}, []);
Проблема: Неконтролируемые обновления
Множественные оповещения подписчиков могут привести к лавинообразным рендерам в UI.
Решение:
- Реализуйте batching — группировку обновлений:
class Newsletter {
// ...
sendUpdate(article) {
// Публикуем через setTimeout для объединения в одну таску
setTimeout(() => {
this.subscribers.forEach(observer => observer.update(article));
}, 0);
}
}
Observer в современных API браузера
- IntersectionObserver
Следит за видимостью элемента:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log("Элемент виден!", entry.target);
}
});
});
observer.observe(document.querySelector(".component"));
- MutationObserver
Реагирует на изменения DOM:
const observer = new MutationObserver(mutations => {
mutations.forEach(mut => console.log("Изменение:", mut.type));
});
observer.observe(document.body, {
childList: true,
attributes: true
});
Когда использовать другие паттерны
- Pub/Sub (Mediator): Подходит для сложных систем с множеством издателей и подписчиков (через централизованный event bus).
- Reactive Programming (RxJS): Для асинхронных потоков данных с операторами (map, filter, debounce).
Выводы
Паттерн Observer — фундамент для событийно-ориентированного кода. Но помните:
- Всегда удаляйте подписки для предотвращения утечек.
- Избегайте тяжёлой логики в
update
— делегируйте её в очередь задач или воркеры. - Используйте нативные API браузера (IntersectionObserver, MutationObserver) вместо самописных решений.
Грамотная реализация Observer делает код предсказуемым, а его композицию — элегантной. Не заставляйте объекты следить друг за другом вручную — делегируйте эту работу паттерну.