Observer в JavaScript: Инициализация и Использование
В JavaScript существует несколько типов Observer API, каждый из которых предназначен для отслеживания различных изменений в веб-приложениях. Рассмотрим основные виды Observer и способы их инициализации.
Паттерн Observer
Классическая реализация паттерна
Паттерн Observer создает зависимость типа «один ко многим» между объектами. Основная идея заключается в том, что при изменении состояния одного объекта (субъекта) все зависящие от него объекты (наблюдатели) автоматически оповещаются об этом.
// Класс наблюдаемого объекта
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!'})Современная реализация с помощью синглтона
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
// Создание наблюдателя с опциями
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)Практический пример: ленивая загрузка изображений
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
// Создание наблюдателя
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
// Создание наблюдателя
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
// Создание наблюдателя производительности
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
// Создание наблюдателя отчетов
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 имеют схожие методы управления:
// Начать наблюдение
observer.observe(element)
// Прекратить наблюдение за конкретным элементом
observer.unobserve(element)
// Прекратить все наблюдения
observer.disconnect()Преимущества и недостатки
Преимущества Observer API
- Разделение ответственности: Наблюдаемые объекты не тесно связаны с наблюдателями
- Асинхронность: Не блокируют основной поток выполнения
- Производительность: Более эффективны чем постоянное опрашивание состояния
- Гибкость: Легко добавлять и удалять наблюдателей в runtime
Недостатки
- Снижение производительности: При большом количестве наблюдателей может значительно упасть производительность
- Сложность отладки: Асинхронное выполнение может усложнить отладку
- Утечки памяти: Необходимо правильно отключать наблюдателей
Observer API предоставляют мощные инструменты для создания отзывчивых и производительных веб-приложений. Правильная инициализация и использование этих API позволяет эффективно реагировать на различные изменения в DOM, производительности и состоянии приложения.
MutationObserver в динамической вёрстке
MutationObserver активно применяется для динамической вёрстки, когда элементы DOM добавляются, изменяются или удаляются без перезагрузки страницы. Вот практические сценарии и примеры кода:
Динамическая подсветка синтаксиса
Когда на страницу через AJAX или JS подгружаются блоки кода, можно автоматически применять к ним подсветку:
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-логику к элементам, которые появляются на странице динамически.
Реализация уведомлений пользователю
Например, вывод баннера, если на странице появилась новая запись (чат, комментарии и т.д.):
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 плагинов для новых карточек):
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):
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.
Контроль изменения классов или стилей
Например, если нужно реагировать на динамическое скрытие/появление или смену оформления элементов:
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 меняется после загрузки страницы.