JS. Начало #10. Observer

Автор: Рябкова Анна, Коптикова Лилия, Шевчук Александра

Что такое паттерн “Наблюдатель” (Observer)

Наблюдатель (англ. Observer) — поведенческий шаблон проектирования. Также известен как «подчинённые». Реализует у класса механизм, который позволяет объекту этого класса получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними.

Классы, на события которых другие классы подписываются, называются субъектами (Subjects), а подписывающиеся классы называются наблюдателями (англ. Observers).

Назначение

Определяет зависимость типа один ко многим между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом событии.

Пример использования Observer в программировании

Допустим, у нас есть сайт, на котором пользователи могут подписываться на различные темы. Когда новая статья по выбранной теме добавляется на сайт, все подписчики должны быть уведомлены об этом.


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(data) {

    this.observers.forEach(observer => observer.update(data));

  }

}

class Observer {

  update(data) {

    console.log('New article is published:', data);

  }

}

const subject = new Subject();

const observer1 = new Observer();

const observer2 = new Observer();

subject.addObserver(observer1);

subject.addObserver(observer2);

subject.notify('Creating a website with Observer pattern');

В этом примере мы создали класс Subject, который отвечает за управление подписчиками (наблюдателями) и их уведомление об изменениях. Класс Observer представляет собой простой наблюдатель, который выводит сообщение о новой статье при получении уведомления.

Зачем применять этот паттерн в JavaScript

Паттерн Observer может быть полезным во многих ситуациях в веб-разработке, например:

  • Обработка событий пользовательского интерфейса (кнопки, формы, навигация и т.д.)
  • Реализация системы уведомлений для пользователей
  • Отслеживание изменений состояния приложения и реагирование на них (например, в одностраничных приложениях)

Основы паттерна Observer

Паттерн Observer состоит из двух основных компонентов: субъекта (Subject) и наблюдателя (Observer). Эти компоненты работают вместе, чтобы обеспечить эффективное и гибкое взаимодействие между различными частями системы.

Субъект (Subject)

Субъект — это объект, за которым наблюдают. Он содержит список наблюдателей и методы для добавления, удаления и уведомления этих наблюдателей о изменениях. Субъект играет ключевую роль в паттерне Observer, так как он управляет списком наблюдателей и отвечает за их уведомление о любых изменениях в своем состоянии.

Наблюдатель (Observer)

Наблюдатель — это объект, который подписывается на изменения субъекта. Он реализует метод обновления, который вызывается субъектом при изменении его состояния. Наблюдатели могут быть различными компонентами системы, которые должны реагировать на изменения состояния субъекта.

Принципы работы

  1. Подписка: Наблюдатели подписываются на субъект, добавляя себя в его список наблюдателей. Это позволяет субъекту знать, какие объекты должны быть уведомлены при изменении его состояния.
  2. Изменение состояния: Когда состояние субъекта изменяется, он уведомляет всех своих наблюдателей. Это может происходить в результате различных событий, таких как изменение данных или выполнение определенных действий.
  3. Обновление: Каждый наблюдатель получает уведомление и обновляет свое состояние в соответствии с изменениями субъекта. Это позволяет наблюдателям реагировать на изменения и выполнять необходимые действия.

Реализация Observer на JavaScript

Создание класса Subject с методами подписки (subscribe) и отписки (unsubscribe)

Subject (Субъект) — это объект, за которым могут следить (подписываться) другие объекты, называемые наблюдателями. Когда внутри Subject происходит какое-то событие, он уведомляет всех своих подписчиков, передавая им нужную информацию.

Чтобы подписчик мог получать уведомления, он должен быть добавлен в список Subject — это делает метод subscribe.

Если подписчику больше не интересны уведомления, он может удалиться из списка — через unsubscribe.

Метод подписки. Добавляет нового наблюдателя в список.



    subscribe(observer) {


      if (typeof observer === 'function') {


        this.observers.push(observer);


      }


    }

Метод отписки. Удаляет наблюдателя из списка.



    unsubscribe(observer) {


      this.observers = this.observers.filter(sub => sub !== observer);


    }

Такой подход широко используется в программировании для построения гибких, масштабируемых систем, где компоненты не зависят напрямую друг от друга.

Добавление механизма уведомления (notify)

Метод notify() является основным компонентом механизма оповещения в паттерне “Наблюдатель”. Он выполняет важную задачу — уведомляет все подписанные наблюдатели о произошедших изменениях. Этот метод:

  1. Проходит по всем подписчикам (наблюдателям) в списке и вызывает их.
  2. Передаёт данные (например, обновления, события) всем подписчикам, чтобы они могли обработать их по-своему.
  3. Позволяет синхронизировать работу разных компонентов в системе, делая их реакцию на изменения независимой от контекста.


    notify(data) {


      this.observers.forEach(observer => observer(data));


    }

​​В данном примере data — это данные, которые передаются каждому наблюдателю.

Метод forEach перебирает всех подписчиков и вызывает их, передавая данные.

Пример кода с простым Observer



    // Субъект, который уведомляет наблюдателей


    class Subject {


      constructor() {


        this.observers = []; // Список подписчиков


      }


      // Метод для подписки (добавление наблюдателя)


      subscribe(observer) {


        if (typeof observer === 'function') {


          this.observers.push(observer);


        }


      }


      // Метод для отписки (удаление наблюдателя)


      unsubscribe(observer) {


        this.observers = this.observers.filter(sub => sub !== observer);


      }


      // Метод для уведомления всех подписчиков


      notify(data) {


        this.observers.forEach(observer => observer(data));


      }


    }


    // Пример наблюдателя


    function observer1(message) {


      console.log('Наблюдатель 1 получил:', message);


    }


    function observer2(message) {


      console.log('Наблюдатель 2 получил:', message);


    }


    // Создаём экземпляр Subject


    const subject = new Subject();


    // Подписываем наблюдателей


    subject.subscribe(observer1);


    subject.subscribe(observer2);


    // Отправляем уведомление всем подписчикам


    console.log('Первое уведомление:');


    subject.notify('Событие 1 произошло');


    // Отписываем одного наблюдателя


    subject.unsubscribe(observer1);


    // Отправляем следующее уведомление


    console.log('\nВторое уведомление:');


    subject.notify('Событие 2 произошло');

Создание объекта Subject: Это объект, который будет следить за состоянием и уведомлять подписчиков.

Метод subscribe: Добавляет наблюдателя в список подписчиков (проверяется, что подписчик — это функция).

Метод unsubscribe: Удаляет наблюдателя из списка.

Метод notify: Уведомляет всех подписчиков, передавая им данные (в данном случае сообщение).

Вывод:

Первое уведомление:

Наблюдатель 1 получил: Событие 1 произошло

Наблюдатель 2 получил: Событие 1 произошло

Второе уведомление:

Наблюдатель 2 получил: Событие 2 произошло

Применение Observer в браузере

Использование MutationObserver для отслеживания изменений в DOM

MutationObserver – это встроенный объект, наблюдающий за DOM-элементом и запускающий колбэк в случае изменений.

MutationObserver очень прост в использовании.

Сначала мы создаем наблюдатель за изменениями с помощью колбэк-функции:

    let observer = new MutationObserver(callback);

Потом прикрепляем его к DOM-узлу:

    observer.observe(node, config);

config – это объект с булевыми параметрами «на какие изменения реагировать»:

  • childList – изменения в непосредственных детях node,
  • subtree – во всех потомках node,
  • attributes – в атрибутах node,
  • attributeFilter – массив имён атрибутов, чтобы наблюдать только за выбранными.
  • characterData – наблюдать ли за node.data (текстовое содержимое)

Затем, после изменений, выполняется callback, в который изменения передаются первым аргументом как список объектов MutationRecord, а сам наблюдатель идёт вторым аргументом.

Объекты MutationRecord имеют следующие свойства:

type – тип изменения, один из:

  • “attributes” изменён атрибут,
  • “characterData” изменены данные elem.data, это для текстовых узлов
  • “childList” добавлены/удалены дочерние элементы,
  • target – где произошло изменение: элемент для “attributes”, текстовый узел для “characterData” или элемент для “childList”,
  • addedNodes/removedNodes – добавленные/удалённые узлы,
  • previousSibling/nextSibling – предыдущий или следующий одноуровневый элемент для добавленных/удалённых элементов,
  • attributeName/attributeNamespace – имя/пространство имён (для XML) изменённого атрибута,
  • oldValue – предыдущее значение, только для изменений атрибута или текста, если включена соответствующая опция attributeOldValue/characterDataOldValue.

Как IntersectionObserver помогает определять видимость элементов

Intersection Observer — браузерный API, который позволяет асинхронно отслеживать пересечение элемента с его родителем или областью видимости документа (viewport). В момент пересечения можно запустить какое-либо действие, например, подгрузить дополнительные посты в ленте новостей («бесконечный скролл») или сделать «ленивую» загрузку контента.

Intersection Observer создаётся с помощью конструктора:

    const observer = new IntersectionObserver(callback, options)

На вход принимает функцию-колбэк, которая будет выполняться при пересечении области и элементов, а также дополнительные настройки пересечения options.

Колбэк принимает два аргумента:

entries — список объектов с информацией о пересечении. Для каждого наблюдаемого элемента создаётся один объект IntersectionObserverEntry.

Объект содержит несколько свойств. Самые полезные:

  • isIntersecting — булево значение. true, если есть пересечение элемента и наблюдаемой области.
  • intersectionRatio — доля пересечения от 0 до 1. Если элемент полностью в наблюдаемой области, то значение будет 1, а если наполовину — 0.5.
  • target — сам наблюдаемый элемент для дальнейших манипуляций. Например, для добавления классов.

Есть и другие свойства, которые позволяют узнать время и координаты пересечения, а также размеры и положение наблюдаемых элементов.

observer — ссылка на экземпляр наблюдателя для вызова методов прослушивания:

  • observe(элемент) — запускает наблюдение за переданным элементом;
  • unobserve(элемент) — убирает элемент из списка наблюдаемых;
  • disconnect() — останавливает наблюдения за всеми элементами.

Пример кода с MutationObserver и Intersection Observer


const container = document.getElementById('container');

    const addBtn = document.getElementById('add');

    // Наблюдает, когда элемент виден внутри контейнера

    const io = new IntersectionObserver(entries => {

      entries.forEach(e => {

        e.target.classList.toggle('visible', e.isIntersecting);

      });

    }, { root: container, threshold: 0.5 });

    // Наблюдает за добавлением новых элементов в контейнер

    const mo = new MutationObserver(mutations => {

      mutations.forEach(m => {

        m.addedNodes.forEach(node => {

          if (node.classList?.contains('box')) io.observe(node);

        });

      });

    });

    mo.observe(container, { childList: true });

    // Кнопка добавляет новый блок

    let i = 1;

    addBtn.onclick = () => {

      const div = document.createElement('div');

      div.className = 'box';

      div.textContent = `Блок ${i++}`;

      container.appendChild(div);

    };

IntersectionObserver — слежение за видимостью

  • Создаёт наблюдателя, который следит за элементами внутри container.
  • e.isIntersecting — true, если элемент на 50% или больше виден.
  • e.target.classList.toggle(‘visible’, e.isIntersecting) — добавляет или убирает класс visible.

MutationObserver — слежение за DOM-изменениями

  • Наблюдает за добавлением новых узлов (элементов) внутрь container.
  • Как только появляется новый элемент с классом box, он автоматически подключается к IntersectionObserver.

Сравнение с EventEmitter и EventListener

В JavaScript существует несколько подходов к реализации подписки на события.

Observer Pattern — паттерн проектирования, где объект (Subject) уведомляет подписчиков (Observers) об изменениях своего состояния.

EventEmitter— это реализация шаблона Publish/Subscribe в Node.js. Он позволяет объектам подписываться на определённые именованные события и реагировать на них. Хотя он близок к Observer, EventEmitter оперирует событиями более абстрактно, без жесткой связи между субъектом и наблюдателем, так как издатель не знает своих подписчиков напрямую — связь происходит через событие по имени.

EventListener — это функция в JavaScript, которая ожидает событие и отвечает на него. Метод addEventListener() используется в браузерах для назначения обработчиков событий на элементы DOM. Он позволяет реагировать на пользовательские действия, такие как клики, ввод текста и другие события интерфейса.

В чем разница между Observer и addEventListener

Observer — это архитектурный паттерн, реализуемый вручную или через библиотеки. Он используется для организации оповещения объектов (наблюдателей) об изменениях состояния других объектов (субъектов). А также для создания собственных систем уведомлений внутри приложения, обеспечивая слабую связанность между компонентами.​

addEventListener предназначен для обработки событий пользовательского интерфейса в браузере. Подходит для взаимодействия с DOM и реагирования на пользовательские действия. Метод addEventListener() работает только с объектами, поддерживающими события (EventTarget). Он не предназначен для создания пользовательских событий вне контекста браузерного DOM.

Когда использовать Observer, а когда события DOM

Observer используется, когда:

  • Необходимо реализовать собственную систему событий или уведомлений.​

  • Требуется слабая связанность между компонентами приложения.​

  • Работаете с фреймворками или библиотеками, поддерживающими реактивное программирование.​

addEventListener используется, когда:

  • Нужно обработать события пользовательского интерфейса в браузере.​

  • Разрабатываете интерактивные элементы на веб-странице.​

  • Требуется стандартный механизм обработки событий DOM

Observer подходит для архитектурных решений внутри приложения, а addEventListener — для взаимодействия с пользователем через интерфейс.​

Рябкова Анна, Коптикова Лилия, Шевчук Александра

Рябкова Анна, Коптикова Лилия, Шевчук Александра