Observer в JavaScript: Инициализация и Использование

В JavaScript существует несколько типов Observer API, каждый из которых предназначен для отслеживания различных изменений в веб-приложениях. Рассмотрим основные виды Observer и способы их инициализации.

Паттерн Observer

Классическая реализация паттерна

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

JavaScript
// Класс наблюдаемого объекта
class EventObserver {
  constructor() {
    this.observers = []
  }

  subscribe(fn) {
    this.observers.push(fn)
  }

  unsubscribe(fn) {
    this.observers = this.observers.filter(subscriber => subscriber !== fn)
  }

  broadcast(data) {
    this.observers.forEach(subscriber => subscriber(data))
  }
}

// Использование
const observer = new EventObserver()

observer.subscribe(data => {
  console.log('Получены данные:', data)
})

observer.broadcast({message: 'Hello World!'})

Современная реализация с помощью синглтона

JavaScript
const observers = []

export default Object.freeze({
  notify: (data) => observers.forEach((observer) => observer(data)),
  subscribe: (func) => observers.push(func),
  unsubscribe: (func) => {
    [...observers].forEach((observer, index) => {
      if (observer === func) {
        observers.splice(index, 1)
      }
    })
  }
})

Web API Observer

Intersection Observer API

Intersection Observer API позволяет асинхронно отслеживать пересечение элемента с его родителем или областью видимости документа.

Инициализация Intersection Observer

JavaScript
// Создание наблюдателя с опциями
const options = {
  root: document.querySelector('#scrollArea'), // Корневой элемент
  rootMargin: '0px',                          // Отступы от области
  threshold: 1.0                              // Порог срабатывания
}

const observer = new IntersectionObserver(callback, options)

// Функция обратного вызова
function callback(entries, observer) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('Элемент стал видимым')
      // Здесь можно загрузить изображение или выполнить другое действие
    }
  })
}

// Начать наблюдение за элементом
const targetElement = document.querySelector('#target')
observer.observe(targetElement)

Практический пример: ленивая загрузка изображений

JavaScript
const lazyImages = document.querySelectorAll('.lazy-image')

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      img.classList.remove('lazy-image')
      observer.unobserve(img)
    }
  })
}, {
  rootMargin: '0px 0px 75px 0px'
})

lazyImages.forEach(img => imageObserver.observe(img))

MutationObserver

MutationObserver наблюдает за изменениями в DOM-дереве.

Инициализация MutationObserver

JavaScript
// Создание наблюдателя
const observer = new MutationObserver(callback)

// Функция обратного вызова
function callback(mutations) {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('Добавлены или удалены дочерние элементы')
    } else if (mutation.type === 'attributes') {
      console.log('Изменен атрибут:', mutation.attributeName)
    }
  })
}

// Конфигурация наблюдения
const config = {
  childList: true,     // Наблюдать за добавлением/удалением детей
  attributes: true,    // Наблюдать за изменениями атрибутов
  subtree: true,       // Наблюдать за всеми потомками
  characterData: true  // Наблюдать за изменениями текста
}

// Начать наблюдение
observer.observe(document.body, config)

ResizeObserver

ResizeObserver отслеживает изменения размеров элементов.

Инициализация ResizeObserver

JavaScript
// Создание наблюдателя
const resizeObserver = new ResizeObserver(entries => {
  for (let entry of entries) {
    const element = entry.target
    const { width, height } = entry.contentRect
    
    console.log(`Элемент изменил размер: ${width}x${height}`)
    
    // Адаптивное поведение
    if (width < 768) {
      element.classList.add('mobile')
    } else {
      element.classList.remove('mobile')
    }
  }
})

// Начать наблюдение за элементом
const element = document.querySelector('#responsive-element')
resizeObserver.observe(element)

PerformanceObserver

PerformanceObserver позволяет наблюдать за метриками производительности.

Инициализация PerformanceObserver

JavaScript
// Создание наблюдателя производительности
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    switch(entry.entryType) {
      case 'navigation':
        console.log(`Страница загружена за ${entry.loadEventEnd}мс`)
        break
      case 'paint':
        console.log(`${entry.name} произошел в ${entry.startTime}мс`)
        break
      case 'largest-contentful-paint':
        console.log(`Основной контент показан в ${entry.startTime}мс`)
        break
    }
  }
})

// Начать наблюдение за различными типами метрик
observer.observe({
  entryTypes: [
    'navigation',
    'paint',
    'largest-contentful-paint',
    'layout-shift'
  ]
})

ReportingObserver

ReportingObserver отслеживает предупреждения и ошибки браузера.

Инициализация ReportingObserver

JavaScript
// Создание наблюдателя отчетов
const observer = new ReportingObserver((reports, observer) => {
  for (const report of reports) {
    console.log('Тип отчета:', report.type)
    console.log('URL:', report.url)
    console.log('Детали:', report.body)
    
    // Отправка отчета на сервер для анализа
    sendToAnalytics(report)
  }
}, {
  types: ['deprecation', 'intervention'],
  buffered: true
})

// Начать наблюдение
observer.observe()

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

Все Web API Observer имеют схожие методы управления:

JavaScript
// Начать наблюдение
observer.observe(element)

// Прекратить наблюдение за конкретным элементом
observer.unobserve(element)

// Прекратить все наблюдения
observer.disconnect()

Преимущества и недостатки

Преимущества Observer API

  • Разделение ответственности: Наблюдаемые объекты не тесно связаны с наблюдателями
  • Асинхронность: Не блокируют основной поток выполнения
  • Производительность: Более эффективны чем постоянное опрашивание состояния
  • Гибкость: Легко добавлять и удалять наблюдателей в runtime

Недостатки

  • Снижение производительности: При большом количестве наблюдателей может значительно упасть производительность
  • Сложность отладки: Асинхронное выполнение может усложнить отладку
  • Утечки памяти: Необходимо правильно отключать наблюдателей

Observer API предоставляют мощные инструменты для создания отзывчивых и производительных веб-приложений. Правильная инициализация и использование этих API позволяет эффективно реагировать на различные изменения в DOM, производительности и состоянии приложения.

MutationObserver в динамической вёрстке

MutationObserver активно применяется для динамической вёрстки, когда элементы DOM добавляются, изменяются или удаляются без перезагрузки страницы. Вот практические сценарии и примеры кода:

Динамическая подсветка синтаксиса
Когда на страницу через AJAX или JS подгружаются блоки кода, можно автоматически применять к ним подсветку:

JavaScript
let observer = new MutationObserver(mutations => {
  for (let mutation of mutations) {
    for (let node of mutation.addedNodes) {
      if (!(node instanceof HTMLElement)) continue;
      if (node.matches('pre[class*="language-"]')) {
        Prism.highlightElement(node);
      } else {
        node.querySelectorAll?.('pre[class*="language-"]').forEach(Prism.highlightElement);
      }
    }
  }
});

// Подключаем к нужному контейнеру
observer.observe(document.getElementById('content'), { childList: true, subtree: true });

Это позволяет применять стили и JS-логику к элементам, которые появляются на странице динамически.

Реализация уведомлений пользователю
Например, вывод баннера, если на странице появилась новая запись (чат, комментарии и т.д.):

JavaScript
let observer = new MutationObserver(mutations => {
  for (let mutation of mutations) {
    if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
      alert("Добавлен новый элемент!");
    }
  }
});

observer.observe(document.getElementById('messages'), { childList: true });

Это удобно для уведомлений или instant UI-реакций на новые данные.

Адаптация контейнеров и “живые” компоненты
Например, нужно автоматически инициализировать некий функционал для вновь появляющихся блоков (например, адаптивные контейнеры или initialize плагинов для новых карточек):

JavaScript
let observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
      if (node.nodeType === 1 && node.classList.contains('card')) {
        initializeCardWidget(node); // пользовательская инициализация
      }
    });
  });
});

observer.observe(document.body, { childList: true, subtree: true });

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

WYSIWYG-редакторы и отслеживание пользовательских изменений
В редакторах можно отслеживать каждое изменение DOM (например, для реализации undo/redo):

JavaScript
let observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    // логируем для истории или анализа
    saveMutationForUndo(mutation);
  });
});

observer.observe(document.getElementById('editor'), {
  childList: true,
  subtree: true,
  characterData: true,
  attributes: true,
});

Это базис для сложных редакторов, динамических форм и realtime UI.

Контроль изменения классов или стилей
Например, если нужно реагировать на динамическое скрытие/появление или смену оформления элементов:

JavaScript
let observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
      // Реакция на изменение класса
      someHandler(mutation.target);
    }
  });
});

observer.observe(document.body, { attributes: true, subtree: true });

Актуально для управления состоянием, анимаций или интеграции сторонних приложений.

Вывод:
MutationObserver незаменим для “реактивной” динамической вёрстки, обработки событий, анимаций и управления сложными интерфейсами, в которых DOM меняется после загрузки страницы.