До этих API разработчики использовали мутационные события (DOMSubtreeModified, DOMNodeInserted) — они были медленными и вызывали проблемы с производительностью, потому что срабатывали синхронно. MutationObserver и ResizeObserver — современная асинхронная альтернатива.
Отслеживает изменения в DOM-дереве. Колбэк вызывается асинхронно, пакетами, после всех мутаций.
const observer = new MutationObserver((mutationList, obs) => {
mutationList.forEach(mutation => {
console.log('Тип мутации:', mutation.type)
if (mutation.type === 'childList') {
console.log('Добавлено:', mutation.addedNodes)
console.log('Удалено:', mutation.removedNodes)
}
if (mutation.type === 'attributes') {
console.log('Атрибут:', mutation.attributeName)
console.log('Старое значение:', mutation.oldValue)
}
})
})observer.observe(targetNode, {
childList: true, // изменения дочерних узлов
attributes: true, // изменения атрибутов
subtree: true, // рекурсивно по всему поддереву
characterData: true, // изменения текстовых узлов
attributeOldValue: true, // сохранять старые значения атрибутов
characterDataOldValue: true, // сохранять старое текстовое содержимое
attributeFilter: ['class', 'style'], // только эти атрибуты
})Каждая запись содержит:
type — 'childList', 'attributes', 'characterData'target — изменённый элементaddedNodes / removedNodes — NodeList добавленных/удалённых узловattributeName — имя изменённого атрибутаoldValue — предыдущее значение// Временно — можно получить накопленные, необработанные записи
const pendingRecords = observer.takeRecords()
// Полностью
observer.disconnect()Отслеживает изменение размеров элементов. Заменяет прослушивание window.resize для конкретных элементов.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect
console.log(`Элемент: ${entry.target.id}`)
console.log(`Новый размер: ${width}×${height}`)
// Адаптивная логика без медиа-запросов
if (width < 400) {
entry.target.classList.add('compact')
} else {
entry.target.classList.remove('compact')
}
})
})
resizeObserver.observe(document.querySelector('.my-component'))entry.contentRect // { width, height, top, left } — контентная область (без padding/border)
entry.borderBoxSize // [{ inlineSize, blockSize }] — с border и padding
entry.contentBoxSize // [{ inlineSize, blockSize }] — только контент
entry.devicePixelContentBoxSize // в физических пикселях устройстваМедиа-запросы реагируют на размер viewport, а не элемента. ResizeObserver позволяет компонентам адаптироваться к своему собственному размеру — это называется Container Queries (теперь есть и CSS @container):
const ro = new ResizeObserver(entries => {
entries.forEach(({ target, contentRect }) => {
target.dataset.size =
contentRect.width < 300 ? 'small' :
contentRect.width < 600 ? 'medium' : 'large'
})
})
ro.observe(document.querySelector('.card'))Оба Observer работают асинхронно и пакетируют изменения. Не вызывают forced layout. Используют меньше ресурсов, чем polling или синхронные event listeners.
Симуляция MutationObserver через событийную систему с виртуальным DOM-деревом
// Симулируем MutationObserver с виртуальными DOM-узлами
class VirtualNode {
constructor(id, tag = 'div') {
this.id = id
this.tag = tag
this.children = []
this.attributes = {}
this.textContent = ''
this._observers = []
}
setAttribute(name, value) {
const oldValue = this.attributes[name]
this.attributes[name] = value
this._notifyObservers('attributes', { attributeName: name, oldValue })
}
appendChild(child) {
this.children.push(child)
this._notifyObservers('childList', { addedNodes: [child], removedNodes: [] })
return child
}
removeChild(child) {
const index = this.children.indexOf(child)
if (index !== -1) {
this.children.splice(index, 1)
this._notifyObservers('childList', { addedNodes: [], removedNodes: [child] })
}
return child
}
setTextContent(text) {
const oldValue = this.textContent
this.textContent = text
this._notifyObservers('characterData', { oldValue })
}
_notifyObservers(type, detail) {
for (const { observer, config, target } of this._observers) {
const shouldNotify =
(type === 'childList' && config.childList) ||
(type === 'attributes' && config.attributes) ||
(type === 'characterData' && config.characterData)
if (shouldNotify) {
const record = { type, target, ...detail }
observer._queueRecord(record)
}
}
// Оповещаем родительских наблюдателей при subtree: true
// (упрощённо — для демонстрации)
}
_addObserver(observer, config) {
this._observers.push({ observer, config, target: this })
}
toString() { return `<${this.tag}#${this.id}>` }
}
class SimulatedMutationObserver {
constructor(callback) {
this._callback = callback
this._queue = []
this._scheduled = false
}
observe(node, config) {
node._addObserver(this, config)
console.log('[MO] Наблюдаем за ' + node + ', config:', JSON.stringify(config))
}
_queueRecord(record) {
this._queue.push(record)
if (!this._scheduled) {
this._scheduled = true
// В браузере это microtask, симулируем синхронно для простоты
Promise.resolve().then(() => {
const records = [...this._queue]
this._queue = []
this._scheduled = false
if (records.length > 0) this._callback(records, this)
})
}
}
takeRecords() {
const records = [...this._queue]
this._queue = []
return records
}
disconnect() {
console.log('[MO] Отключён')
}
}
// Демо
const root = new VirtualNode('app')
const list = new VirtualNode('list', 'ul')
const header = new VirtualNode('header', 'h1')
const observer = new SimulatedMutationObserver((mutations) => {
console.log(`\n[Callback] Получено ${mutations.length} мутаций:`)
mutations.forEach(m => {
if (m.type === 'childList') {
const added = m.addedNodes.map(n => `${n}`).join(', ')
const removed = m.removedNodes.map(n => `${n}`).join(', ')
if (added) console.log(` + Добавлены: ${added} в ${m.target}`)
if (removed) console.log(` - Удалены: ${removed} из ${m.target}`)
}
if (m.type === 'attributes') {
console.log(` ~ Атрибут "${m.attributeName}" изменён в ${m.target} (было: ${m.oldValue})`)
}
})
})
observer.observe(root, { childList: true, attributes: false })
observer.observe(list, { childList: true, attributes: true })
root.appendChild(header)
root.appendChild(list)
const item1 = new VirtualNode('item-1', 'li')
const item2 = new VirtualNode('item-2', 'li')
list.appendChild(item1)
list.appendChild(item2)
list.setAttribute('class', 'active-list')
list.setAttribute('class', 'inactive-list')
// Ждём microtask
await Promise.resolve()
await Promise.resolve()
list.removeChild(item1)
await Promise.resolve()До этих API разработчики использовали мутационные события (DOMSubtreeModified, DOMNodeInserted) — они были медленными и вызывали проблемы с производительностью, потому что срабатывали синхронно. MutationObserver и ResizeObserver — современная асинхронная альтернатива.
Отслеживает изменения в DOM-дереве. Колбэк вызывается асинхронно, пакетами, после всех мутаций.
const observer = new MutationObserver((mutationList, obs) => {
mutationList.forEach(mutation => {
console.log('Тип мутации:', mutation.type)
if (mutation.type === 'childList') {
console.log('Добавлено:', mutation.addedNodes)
console.log('Удалено:', mutation.removedNodes)
}
if (mutation.type === 'attributes') {
console.log('Атрибут:', mutation.attributeName)
console.log('Старое значение:', mutation.oldValue)
}
})
})observer.observe(targetNode, {
childList: true, // изменения дочерних узлов
attributes: true, // изменения атрибутов
subtree: true, // рекурсивно по всему поддереву
characterData: true, // изменения текстовых узлов
attributeOldValue: true, // сохранять старые значения атрибутов
characterDataOldValue: true, // сохранять старое текстовое содержимое
attributeFilter: ['class', 'style'], // только эти атрибуты
})Каждая запись содержит:
type — 'childList', 'attributes', 'characterData'target — изменённый элементaddedNodes / removedNodes — NodeList добавленных/удалённых узловattributeName — имя изменённого атрибутаoldValue — предыдущее значение// Временно — можно получить накопленные, необработанные записи
const pendingRecords = observer.takeRecords()
// Полностью
observer.disconnect()Отслеживает изменение размеров элементов. Заменяет прослушивание window.resize для конкретных элементов.
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect
console.log(`Элемент: ${entry.target.id}`)
console.log(`Новый размер: ${width}×${height}`)
// Адаптивная логика без медиа-запросов
if (width < 400) {
entry.target.classList.add('compact')
} else {
entry.target.classList.remove('compact')
}
})
})
resizeObserver.observe(document.querySelector('.my-component'))entry.contentRect // { width, height, top, left } — контентная область (без padding/border)
entry.borderBoxSize // [{ inlineSize, blockSize }] — с border и padding
entry.contentBoxSize // [{ inlineSize, blockSize }] — только контент
entry.devicePixelContentBoxSize // в физических пикселях устройстваМедиа-запросы реагируют на размер viewport, а не элемента. ResizeObserver позволяет компонентам адаптироваться к своему собственному размеру — это называется Container Queries (теперь есть и CSS @container):
const ro = new ResizeObserver(entries => {
entries.forEach(({ target, contentRect }) => {
target.dataset.size =
contentRect.width < 300 ? 'small' :
contentRect.width < 600 ? 'medium' : 'large'
})
})
ro.observe(document.querySelector('.card'))Оба Observer работают асинхронно и пакетируют изменения. Не вызывают forced layout. Используют меньше ресурсов, чем polling или синхронные event listeners.
Симуляция MutationObserver через событийную систему с виртуальным DOM-деревом
// Симулируем MutationObserver с виртуальными DOM-узлами
class VirtualNode {
constructor(id, tag = 'div') {
this.id = id
this.tag = tag
this.children = []
this.attributes = {}
this.textContent = ''
this._observers = []
}
setAttribute(name, value) {
const oldValue = this.attributes[name]
this.attributes[name] = value
this._notifyObservers('attributes', { attributeName: name, oldValue })
}
appendChild(child) {
this.children.push(child)
this._notifyObservers('childList', { addedNodes: [child], removedNodes: [] })
return child
}
removeChild(child) {
const index = this.children.indexOf(child)
if (index !== -1) {
this.children.splice(index, 1)
this._notifyObservers('childList', { addedNodes: [], removedNodes: [child] })
}
return child
}
setTextContent(text) {
const oldValue = this.textContent
this.textContent = text
this._notifyObservers('characterData', { oldValue })
}
_notifyObservers(type, detail) {
for (const { observer, config, target } of this._observers) {
const shouldNotify =
(type === 'childList' && config.childList) ||
(type === 'attributes' && config.attributes) ||
(type === 'characterData' && config.characterData)
if (shouldNotify) {
const record = { type, target, ...detail }
observer._queueRecord(record)
}
}
// Оповещаем родительских наблюдателей при subtree: true
// (упрощённо — для демонстрации)
}
_addObserver(observer, config) {
this._observers.push({ observer, config, target: this })
}
toString() { return `<${this.tag}#${this.id}>` }
}
class SimulatedMutationObserver {
constructor(callback) {
this._callback = callback
this._queue = []
this._scheduled = false
}
observe(node, config) {
node._addObserver(this, config)
console.log('[MO] Наблюдаем за ' + node + ', config:', JSON.stringify(config))
}
_queueRecord(record) {
this._queue.push(record)
if (!this._scheduled) {
this._scheduled = true
// В браузере это microtask, симулируем синхронно для простоты
Promise.resolve().then(() => {
const records = [...this._queue]
this._queue = []
this._scheduled = false
if (records.length > 0) this._callback(records, this)
})
}
}
takeRecords() {
const records = [...this._queue]
this._queue = []
return records
}
disconnect() {
console.log('[MO] Отключён')
}
}
// Демо
const root = new VirtualNode('app')
const list = new VirtualNode('list', 'ul')
const header = new VirtualNode('header', 'h1')
const observer = new SimulatedMutationObserver((mutations) => {
console.log(`\n[Callback] Получено ${mutations.length} мутаций:`)
mutations.forEach(m => {
if (m.type === 'childList') {
const added = m.addedNodes.map(n => `${n}`).join(', ')
const removed = m.removedNodes.map(n => `${n}`).join(', ')
if (added) console.log(` + Добавлены: ${added} в ${m.target}`)
if (removed) console.log(` - Удалены: ${removed} из ${m.target}`)
}
if (m.type === 'attributes') {
console.log(` ~ Атрибут "${m.attributeName}" изменён в ${m.target} (было: ${m.oldValue})`)
}
})
})
observer.observe(root, { childList: true, attributes: false })
observer.observe(list, { childList: true, attributes: true })
root.appendChild(header)
root.appendChild(list)
const item1 = new VirtualNode('item-1', 'li')
const item2 = new VirtualNode('item-2', 'li')
list.appendChild(item1)
list.appendChild(item2)
list.setAttribute('class', 'active-list')
list.setAttribute('class', 'inactive-list')
// Ждём microtask
await Promise.resolve()
await Promise.resolve()
list.removeChild(item1)
await Promise.resolve()Реализуй createDOMWatcher() с методами: watchNode(nodeId, config) где config — { childList: boolean, attributes: boolean }, начинает отслеживать узел; simulateChange(nodeId, changeType, detail) симулирует мутацию (changeType: "childList" | "attributes", detail: { addedNode? } или { attributeName?, oldValue? }); getRecords() возвращает массив всех накопленных записей мутаций в формате { nodeId, type, detail }.
watchers.set(nodeId, config) сохраняет конфигурацию. В simulateChange() получай config через watchers.get(nodeId). Проверяй config.childList для childList и config.attributes для attributes. getRecords() возвращает [...records] чтобы вернуть копию.