8 паттернов реактивности в современном JavaScript.
Реактивность — это то, как система реагирует на изменения данных. Выделяют различные типы реактивности. В этой статье сосредоточимся на реактивности в плане выполнения действий в ответ на изменение данных.
Как фронтенд-разработчику, мне приходится сталкиваться с этим ежедневно, поскольку браузер сам по себе является полностью асинхронной средой. Современные веб-интерфейсы должны быстро реагировать на действия пользователя, ч
8 паттернов реактивности в современном JavaScript...
Реактивность — это то, как система реагирует на изменения данных. Выделяют различные типы реактивности. В этой статье сосредоточимся на реактивности в плане выполнения действий в ответ на изменение данных.
Как фронтенд-разработчику, мне приходится сталкиваться с этим ежедневно, поскольку браузер сам по себе является полностью асинхронной средой. Современные веб-интерфейсы должны быстро реагировать на действия пользователя, что включает обновление пользовательского интерфейса, отправку сетевых запросов, управление навигацией и выполнение многих других задач.
Хотя реактивность часто ассоциируется с фреймворками, уверен, что многому можно научиться, реализуя ее в чистом JS. Поэтому мы сами напишем кодовые паттерны, а также изучим некоторые нативные браузерные API, основанные на реактивности.
Pub/Sub
Pub/Sub (Publish/Subscribe — издатель/подписчик) — один из наиболее часто используемых фундаментальных паттернов реактивности. Издатель отвечает за уведомление подписчика об обновлениях, а подписчик получает эти обновления и должен реагировать на них.
class PubSub { constructor() { this.subscribers = {}; } subscribe(event, callback) { if (!this.subscribers[event]) { this.subscribers[event] = []; } this.subscribers[event].push(callback); } // Публикация сообщения об определенном событии для всех подписчиков publish(event, data) { if (this.subscribers[event]) { this.subscribers[event].forEach((callback) => { callback(data); }); } } } const pubsub = new PubSub(); pubsub.subscribe('news', (message) => { console.log(`Subscriber 1 received news: ${message}`); }); pubsub.subscribe('news', (message) => { console.log(`Subscriber 2 received news: ${message}`); }); // Публикация сообщения к событию 'news' pubsub.publish('news', 'Latest headlines: ...'); // Записи в журнале консоли: // Subscriber 1 received news: Latest headlines: ... // Subscriber 2 received news: Latest headlines: ...
Одним из популярных примеров его использования является Redux. Эта популярная библиотека управления состояниями основана на данном паттерне (а точнее, на архитектуре Flux). В контексте Redux все работает довольно просто.
- Издатель: в роли издателя выступает хранилище. Когда происходит отправка действия, хранилище уведомляет все подписанные компоненты об изменении состояния.
- Подписчик: в роли подписчиков выступают компоненты пользовательского интерфейса приложения. Подписанные на хранилище Redux, они получают обновления при каждом изменении состояния.
Custom Events — браузерная версия Pub/Sub
Браузер предлагает API для запуска и подписки на Custom Events (пользовательские события) с помощью класса CustomEvent и метода dispatchEvent. Последний дает возможность не только инициировать событие, но и прикреплять к нему любые данные.
const customEvent = new CustomEvent('customEvent', { detail: 'Custom event data', // Прикрепление нужных данных к событию }); const element = document.getElementById('.element-to-trigger-events'); element.addEventListener('customEvent', (event) => { console.log(`Subscriber 1 received custom event: ${event.detail}`); }); element.addEventListener('customEvent', (event) => { console.log(`Subscriber 2 received custom event: ${event.detail}`); }); // Запуск пользовательского события element.dispatchEvent(customEvent); // Записи в журнале консоли: // Subscriber 1 received custom event: Custom event data // Subscriber 2 received custom event: Custom event data
Custom Event Targets
Если вы предпочитаете не отправлять события глобально на объект window, можно создавать собственные Event Targets (цели событий).
Расширив нативный класс EventTarget, можно отправлять события в его новый экземпляр. Таким образом, события будут срабатывать только в новом классе, избегая глобального распространения. Более того, у вас будет возможность прикреплять обработчики непосредственно к этому конкретному экземпляру.
class CustomEventTarget extends EventTarget { constructor() { super(); } // Пользовательский метод для запуска событий triggerCustomEvent(eventName, eventData) { const event = new CustomEvent(eventName, { detail: eventData }); this.dispatchEvent(event); } } const customTarget = new CustomEventTarget(); // Добавление слушателя событий к цели пользоваельского события customTarget.addEventListener('customEvent', (event) => { console.log(`Custom event received with data: ${event.detail}`); }); // Запуск пользовательского события customTarget.triggerCustomEvent('customEvent', 'Hello, custom event!'); // Запись в журнале консоли: // Custom event received with data: Hello, custom event!
Observer
Паттерн Observer (наблюдатель) очень похож на pub/sub. Он позволяет подписываться на субъект (Subject), который уведомляет своих подписчиков (Observers — наблюдателей) об изменениях, обеспечивая их реагирование соответствующим образом. Этот паттерн играет важную роль в создании несвязанной и гибкой архитектуры.
class Subject { constructor() { this.observers = []; } addObserver(observer) { this.observers.push(observer); } // Удаление наблюдателя из списка removeObserver(observer) { const index = this.observers.indexOf(observer); if (index !== -1) { this.observers.splice(index, 1); } } // Уведомление всех наблюдателей об изменениях notify() { this.observers.forEach((observer) => { observer.update(); }); } } class Observer { constructor(name) { this.name = name; } // Вызов метода update при получении уведомления update() { console.log(`${this.name} received an update.`); } } const subject = new Subject(); const observer1 = new Observer('Observer 1'); const observer2 = new Observer('Observer 2'); // Добавление наблюдателей к субъекту subject.addObserver(observer1); subject.addObserver(observer2); // Уведомление наблюдателей об изменениях subject.notify(); // Записи в журнале консоли: // Observer 1 received an update. // Observer 2 received an update.
Proxy — обеспечение реактивности свойств
Реагирование на изменения в объектах обеспечивает Proxy (прокси). Этот паттерн позволяет добиться реактивности при установке или получении значений полей объекта.
const person = { name: 'Pavel', age: 22, }; const reactivePerson = new Proxy(person, { // Перехват операции set (установки) set(target, key, value) { console.log(`Setting ${key} to ${value}`); target[key] = value; // Указывает, была ли успешной операция установки return true; }, // Перехват операции get (получения) get(target, key) { console.log(`Getting ${key}`); return target[key]; }, }); reactivePerson.name = 'Sergei'; // Установка имени Сергей console.log(reactivePerson.name); // Получение имени: Сергей reactivePerson.age = 23; // Установка возраста - 23 года console.log(reactivePerson.age); // Получение возраста: 23 года
Object.defineProperties — реактивность отдельных свойств
Если вам не нужно отслеживать все поля в объектах, можете выбрать конкретное поле с помощью Object.defineProperty или группу полей с помощью Object.defineProperties.
const person = { _originalName: 'Pavel', // приватное свойство } Object.defineProperty(person, 'name', { get() { console.log('Getting property name') return this._originalName }, set(value) { console.log(`Setting property name to value ${value}`) this._originalName = value }, }) console.log(person.name) // 'Поучение имени свойства' и 'Павел' person.name = 'Sergei' // Установка имени свойства в значение "Сергей"
MutationObserver — реактивность HTML-атрибутов
Один из способов добиться реактивности в DOM — использовать MutationObserver. Его API позволяет наблюдать за изменениями в атрибутах, а также в текстовом содержимом целевого элемента и его дочерних элементов.
function handleMutations(mutationsList, observer) { mutationsList.forEach((mutation) => { // Атрибут наблюдаемого элемента изменился if (mutation.type === 'attributes') { console.log(`Attribute '${mutation.attributeName}' changed to '${mutation.target.getAttribute(mutation.attributeName)}'`); } }); } const observer = new MutationObserver(handleMutations); const targetElement = document.querySelector('.element-to-observe'); // Начало наблюдения за целевым элементом observer.observe(targetElement, { attributes: true });
IntersectionObserver — реактивность скроллинга
API IntersectionObserver позволяет реагировать на пересечение целевого элемента с другим элементом или областью окна просмотра.
function handleIntersection(entries, observer) { entries.forEach((entry) => { // Целевой элемент находится в окне просмотра if (entry.isIntersecting) { entry.target.classList.add('visible'); } else { entry.target.classList.remove('visible'); } }); } const observer = new IntersectionObserver(handleIntersection); const targetElement = document.querySelector('.element-to-observe'); // Начало наблюдения за целевым элементом observer.observe(targetElement);