Вы встраиваете виджет аналитики в чужой сайт: сторонний скрипт динамически добавляет контент через AJAX. Вы не можете изменить их код. Нужно реагировать когда они добавят новые элементы. Опросы через setInterval — расточительно. Решение: MutationObserver — реактивное наблюдение за изменениями DOM.
MutationObserver — API браузера для наблюдения за изменениями в DOM без постоянного опроса. Срабатывает только когда что-то реально изменилось, эффективнее setInterval.
// Создаём наблюдатель — передаём callback
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
console.log('Тип изменения:', mutation.type)
// 'childList' | 'attributes' | 'characterData'
}
})const targetElement = document.getElementById('chat-messages')
observer.observe(targetElement, {
childList: true, // следить за добавлением/удалением дочерних элементов
attributes: true, // следить за изменением атрибутов
subtree: true, // наблюдать за всем поддеревом, не только прямыми детьми
characterData: true, // следить за изменением текстового содержимого
attributeFilter: ['class', 'style'], // только эти атрибуты (опционально)
})Callback получает массив объектов MutationRecord:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log(mutation.type) // тип изменения
console.log(mutation.target) // элемент, который изменился
console.log(mutation.addedNodes) // добавленные узлы (NodeList)
console.log(mutation.removedNodes) // удалённые узлы (NodeList)
console.log(mutation.attributeName) // имя изменённого атрибута
console.log(mutation.oldValue) // старое значение (если включено)
})
})// Важно отключать наблюдатель, когда он не нужен — иначе утечка памяти
observer.disconnect()const lazyImageObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const images = node.querySelectorAll('img[data-src]')
images.forEach(img => {
img.src = img.dataset.src // загружаем реальное изображение
delete img.dataset.src
})
})
})
})
lazyImageObserver.observe(document.body, { childList: true, subtree: true })// Сторонний скрипт меняет DOM — мы должны реагировать
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Новый контент добавлен — обновляем наш виджет
updateWidget(mutation.target)
}
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
// Класс изменился — обновляем стили нашего виджета
syncStyles(mutation.target)
}
}
})
observer.observe(document.querySelector('.cms-content'), {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['class'],
})| | MutationObserver | DOM события |
|---|---|---|
| Когда срабатывает | После всех изменений (batch) | Синхронно при каждом изменении |
| Производительность | Высокая | Может быть ниже при частых изменениях |
| Что отслеживает | Структурные изменения DOM | Пользовательские действия |
| Доступность | Только браузер | Только браузер |
Ошибка 1: забывают disconnect() — утечка памяти
// Сломано: observer живёт вечно, даже когда элемент удалён из DOM
const observer = new MutationObserver(callback)
observer.observe(element, { childList: true })
// element удалён, но observer продолжает держать ссылку
// Исправлено: отключать в cleanup
function attachObserver(element) {
const observer = new MutationObserver(callback)
observer.observe(element, { childList: true })
return () => observer.disconnect() // возвращаем функцию отписки
}
const cleanup = attachObserver(chatContainer)
// При размонтировании:
cleanup()Ошибка 2: subtree: true без необходимости
// Сломано: наблюдаем за всем document.body — получим тысячи событий
observer.observe(document.body, { childList: true, subtree: true })
// Исправлено: наблюдаем за конкретным элементом
observer.observe(document.getElementById('chat-messages'), { childList: true })Ошибка 3: мутации внутри callback создают бесконечный цикл
// Сломано: callback меняет DOM → наблюдатель видит изменение → снова callback...
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => {
m.target.setAttribute('data-processed', 'true') // вызовет новую мутацию!
})
})
observer.observe(el, { attributes: true })
// Исправлено: отключаем перед изменением, включаем после
const observer = new MutationObserver((mutations) => {
observer.disconnect() // пауза
mutations.forEach(m => {
if (!m.target.dataset.processed) {
m.target.dataset.processed = 'true'
}
})
observer.observe(el, { attributes: true }) // снова включаем
})<code> блокиВ этой sandbox нет DOM — следующий пример демонстрирует тот же паттерн реактивного наблюдения через Proxy.
Симуляция паттерна MutationObserver через наблюдаемый объект на основе Proxy
// Симуляция паттерна MutationObserver для демонстрации в sandbox (без DOM)
// Та же идея: наблюдатель реагирует на изменения объекта (вместо DOM-узла)
class MockMutationObserver {
constructor(callback) {
this._callback = callback
this._targets = new Map()
}
// observe(target, options) — начать наблюдение за объектом
observe(target, options = {}) {
const proxy = new Proxy(target, {
set: (obj, prop, value) => {
const oldValue = obj[prop]
obj[prop] = value
// Создаём MutationRecord-подобный объект
const record = {
type: 'property',
target: obj,
property: prop,
oldValue,
newValue: value,
}
// Вызываем callback с массивом записей (как настоящий MutationObserver)
this._callback([record], this)
return true
},
})
this._targets.set(target, proxy)
return proxy // возвращаем proxy-версию для использования
}
// disconnect() — прекратить наблюдение
disconnect() {
this._targets.clear()
console.log('Наблюдение прекращено')
}
}
// --- Демонстрация ---
// Наш "DOM-элемент" — обычный объект
const chatContainer = {
messageCount: 0,
lastMessage: null,
unreadCount: 0,
}
// Создаём наблюдатель
const observer = new MockMutationObserver((mutations) => {
for (const mutation of mutations) {
const { property, oldValue, newValue } = mutation
if (property === 'messageCount') {
console.log(`[Observer] Новых сообщений: ${newValue} (было: ${oldValue})`)
}
if (property === 'lastMessage') {
console.log(`[Observer] Последнее сообщение: "${newValue}"`)
}
if (property === 'unreadCount' && newValue > 0) {
console.log(`[Observer] Обновление badge: ${newValue} непрочитанных`)
}
}
})
// Начинаем наблюдение — получаем proxy-версию объекта
const observedChat = observer.observe(chatContainer)
// Изменяем объект — наблюдатель реагирует автоматически
console.log('--- Приходят новые сообщения ---\n')
observedChat.lastMessage = 'Привет! Как дела?'
observedChat.messageCount = 1
observedChat.unreadCount = 1
observedChat.lastMessage = 'Готов к встрече в 15:00?'
observedChat.messageCount = 2
observedChat.unreadCount = 2
// Пользователь прочитал сообщения
console.log('\n--- Пользователь прочитал сообщения ---\n')
observedChat.unreadCount = 0
// Отключаем наблюдатель
console.log('\n--- Отключаем наблюдатель ---')
observer.disconnect()
// Изменения больше не отслеживаются
observedChat.messageCount = 5
console.log('Изменение после disconnect не вызвало callback')Вы встраиваете виджет аналитики в чужой сайт: сторонний скрипт динамически добавляет контент через AJAX. Вы не можете изменить их код. Нужно реагировать когда они добавят новые элементы. Опросы через setInterval — расточительно. Решение: MutationObserver — реактивное наблюдение за изменениями DOM.
MutationObserver — API браузера для наблюдения за изменениями в DOM без постоянного опроса. Срабатывает только когда что-то реально изменилось, эффективнее setInterval.
// Создаём наблюдатель — передаём callback
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
console.log('Тип изменения:', mutation.type)
// 'childList' | 'attributes' | 'characterData'
}
})const targetElement = document.getElementById('chat-messages')
observer.observe(targetElement, {
childList: true, // следить за добавлением/удалением дочерних элементов
attributes: true, // следить за изменением атрибутов
subtree: true, // наблюдать за всем поддеревом, не только прямыми детьми
characterData: true, // следить за изменением текстового содержимого
attributeFilter: ['class', 'style'], // только эти атрибуты (опционально)
})Callback получает массив объектов MutationRecord:
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log(mutation.type) // тип изменения
console.log(mutation.target) // элемент, который изменился
console.log(mutation.addedNodes) // добавленные узлы (NodeList)
console.log(mutation.removedNodes) // удалённые узлы (NodeList)
console.log(mutation.attributeName) // имя изменённого атрибута
console.log(mutation.oldValue) // старое значение (если включено)
})
})// Важно отключать наблюдатель, когда он не нужен — иначе утечка памяти
observer.disconnect()const lazyImageObserver = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return
const images = node.querySelectorAll('img[data-src]')
images.forEach(img => {
img.src = img.dataset.src // загружаем реальное изображение
delete img.dataset.src
})
})
})
})
lazyImageObserver.observe(document.body, { childList: true, subtree: true })// Сторонний скрипт меняет DOM — мы должны реагировать
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
// Новый контент добавлен — обновляем наш виджет
updateWidget(mutation.target)
}
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
// Класс изменился — обновляем стили нашего виджета
syncStyles(mutation.target)
}
}
})
observer.observe(document.querySelector('.cms-content'), {
childList: true,
attributes: true,
subtree: true,
attributeFilter: ['class'],
})| | MutationObserver | DOM события |
|---|---|---|
| Когда срабатывает | После всех изменений (batch) | Синхронно при каждом изменении |
| Производительность | Высокая | Может быть ниже при частых изменениях |
| Что отслеживает | Структурные изменения DOM | Пользовательские действия |
| Доступность | Только браузер | Только браузер |
Ошибка 1: забывают disconnect() — утечка памяти
// Сломано: observer живёт вечно, даже когда элемент удалён из DOM
const observer = new MutationObserver(callback)
observer.observe(element, { childList: true })
// element удалён, но observer продолжает держать ссылку
// Исправлено: отключать в cleanup
function attachObserver(element) {
const observer = new MutationObserver(callback)
observer.observe(element, { childList: true })
return () => observer.disconnect() // возвращаем функцию отписки
}
const cleanup = attachObserver(chatContainer)
// При размонтировании:
cleanup()Ошибка 2: subtree: true без необходимости
// Сломано: наблюдаем за всем document.body — получим тысячи событий
observer.observe(document.body, { childList: true, subtree: true })
// Исправлено: наблюдаем за конкретным элементом
observer.observe(document.getElementById('chat-messages'), { childList: true })Ошибка 3: мутации внутри callback создают бесконечный цикл
// Сломано: callback меняет DOM → наблюдатель видит изменение → снова callback...
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => {
m.target.setAttribute('data-processed', 'true') // вызовет новую мутацию!
})
})
observer.observe(el, { attributes: true })
// Исправлено: отключаем перед изменением, включаем после
const observer = new MutationObserver((mutations) => {
observer.disconnect() // пауза
mutations.forEach(m => {
if (!m.target.dataset.processed) {
m.target.dataset.processed = 'true'
}
})
observer.observe(el, { attributes: true }) // снова включаем
})<code> блокиВ этой sandbox нет DOM — следующий пример демонстрирует тот же паттерн реактивного наблюдения через Proxy.
Симуляция паттерна MutationObserver через наблюдаемый объект на основе Proxy
// Симуляция паттерна MutationObserver для демонстрации в sandbox (без DOM)
// Та же идея: наблюдатель реагирует на изменения объекта (вместо DOM-узла)
class MockMutationObserver {
constructor(callback) {
this._callback = callback
this._targets = new Map()
}
// observe(target, options) — начать наблюдение за объектом
observe(target, options = {}) {
const proxy = new Proxy(target, {
set: (obj, prop, value) => {
const oldValue = obj[prop]
obj[prop] = value
// Создаём MutationRecord-подобный объект
const record = {
type: 'property',
target: obj,
property: prop,
oldValue,
newValue: value,
}
// Вызываем callback с массивом записей (как настоящий MutationObserver)
this._callback([record], this)
return true
},
})
this._targets.set(target, proxy)
return proxy // возвращаем proxy-версию для использования
}
// disconnect() — прекратить наблюдение
disconnect() {
this._targets.clear()
console.log('Наблюдение прекращено')
}
}
// --- Демонстрация ---
// Наш "DOM-элемент" — обычный объект
const chatContainer = {
messageCount: 0,
lastMessage: null,
unreadCount: 0,
}
// Создаём наблюдатель
const observer = new MockMutationObserver((mutations) => {
for (const mutation of mutations) {
const { property, oldValue, newValue } = mutation
if (property === 'messageCount') {
console.log(`[Observer] Новых сообщений: ${newValue} (было: ${oldValue})`)
}
if (property === 'lastMessage') {
console.log(`[Observer] Последнее сообщение: "${newValue}"`)
}
if (property === 'unreadCount' && newValue > 0) {
console.log(`[Observer] Обновление badge: ${newValue} непрочитанных`)
}
}
})
// Начинаем наблюдение — получаем proxy-версию объекта
const observedChat = observer.observe(chatContainer)
// Изменяем объект — наблюдатель реагирует автоматически
console.log('--- Приходят новые сообщения ---\n')
observedChat.lastMessage = 'Привет! Как дела?'
observedChat.messageCount = 1
observedChat.unreadCount = 1
observedChat.lastMessage = 'Готов к встрече в 15:00?'
observedChat.messageCount = 2
observedChat.unreadCount = 2
// Пользователь прочитал сообщения
console.log('\n--- Пользователь прочитал сообщения ---\n')
observedChat.unreadCount = 0
// Отключаем наблюдатель
console.log('\n--- Отключаем наблюдатель ---')
observer.disconnect()
// Изменения больше не отслеживаются
observedChat.messageCount = 5
console.log('Изменение после disconnect не вызвало callback')Реализуй класс Observable, который принимает объект и возвращает реактивную версию. При изменении любого свойства Observable должен уведомлять всех подписанных наблюдателей, вызывая их с объектом { property, oldValue, newValue }. Реализуй методы subscribe(callback) и unsubscribe(callback).
Use Proxy to intercept set operations, call registered callbacks with { property, oldValue, newValue }. subscribe: this._observers.push(callback); notify: this._observers.forEach(cb => cb({ property, oldValue, newValue }))