ResizeObserver: аналог document.onresize для элементов
ResizeObserver позволяет узнать, когда изменяется размер элемента.
До появления ResizeObserver вам приходилось прикреплять прослушиватель к событию документа resize, чтобы получать уведомления обо всех изменениях размеров области просмотра. Затем нужно было понять, какие элементы были затронуты этим изменением, и вызвать конкретную подпрограмму в обработчике событий, чтобы отреагировать соответствующим образом. Если вам требовались новые размеры элемента после изменения размера, нужно было вызвать getBoundingClientRect() или getComputedStyle(), что могло вызвать сбой макета, если не позаботиться о пакетной обработке всех чтений и всех записей.
И все это не предусматривало случаи, когда меняется размер элементов без изменения размера главного окна. Например, добавление новых дочерних элементов, установка стиля элемента display в значение none или аналогичные действия могут изменить размер элемента, его предков или элементов одного с ним уровня.
Поэтому ResizeObserver — полезный примитив. Он реагирует на изменение размера любого из наблюдаемых элементов, независимо от того, что вызвало это изменение. Он также обеспечивает доступ к новому размеру наблюдаемых элементов.
API #
У всех API с суффиксом Observer, упомянутых выше, простой дизайн интерфейса. ResizeObserver — не исключение. Вы создаете объект ResizeObserver и передаете обратный вызов конструктору. Обратному вызову передается массив объектов ResizeObserverEntry — по одной записи на каждый наблюдаемый элемент — который содержит новые размеры элемента.
var ro = new ResizeObserver(entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Наблюдаем один или несколько элементов
ro.observe(someElement);
Некоторые подробности #
О чем сообщается? #
Как правило, ResizeObserverEntry сообщает о блоке содержимого элемента через свойство с именем contentRect, которое возвращает объект DOMRectReadOnly. Блок содержимого — это область внутри рамки с учетом внутреннего отступа.

Важно отметить, что, хотя ResizeObserver сообщает и размеры contentRect, и внутренние отступы, он наблюдает только contentRect. Не путайте contentRect с ограничивающей рамкой элемента. Ограничивающая рамка, как сообщает getBoundingClientRect(), представляет собой прямоугольник, который содержит весь элемент и его потомков. SVG являются исключением из правила, где ResizeObserver сообщает размеры ограничивающей рамки.
Начиная с Chrome 84, ResizeObserverEntry имеет три новых свойства для предоставления более подробной информации. Каждое из этих свойств возвращает объект ResizeObserverSize, содержащий свойства blockSize и inlineSize. Эта информация касается наблюдаемого элемента во время обратного вызова.
borderBoxSizecontentBoxSizedevicePixelContentBoxSize
Все эти элементы возвращают массивы только для чтения, так как в будущем ожидается, что они смогут поддерживать элементы, имеющие несколько фрагментов, что встречается в сценариях с несколькими столбцами. На данный момент эти массивы будут содержать только один элемент.
Поддержка этих свойств платформой ограничена, но Firefox уже поддерживает первые два.
Когда об этом сообщается? #
В спецификации указано, что ResizeObserver должен обрабатывать все события изменения размера после макетирования и до отрисовки. Благодаря этому обратный вызов ResizeObserver становится идеальным местом для внесения изменений в макет вашей страницы. Поскольку обработка ResizeObserver происходит между макетированием и отрисовкой, такой подход аннулирует только макет, но не отрисовку.
Подводные камни #
Вы можете задаться вопросом: что произойдет, если изменить размер наблюдаемого элемента внутри обратного вызова ResizeObserver? Ответ такой — сразу же будет инициирован еще один обратный вызов. К счастью, в ResizeObserver есть механизм, позволяющий избежать бесконечных циклов обратного вызова и циклических зависимостей. Изменения будут обрабатываться в том же кадре только в том случае, если измененный элемент находится глубже в дереве DOM, чем наименее глубоко расположенный элемент, обработанный в предыдущем обратном вызове. В противном случае они будут отложены до следующего кадра.
Применение #
ResizeObserver позволяет реализовывать поэлементные медиазапросы. Наблюдая за элементами, вы можете императивно определять контрольные точки своего дизайна и изменять стили элемента. В следующем примере второй блок изменит радиус границы в соответствии с его шириной.
const ro = new ResizeObserver(entries => {
for (let entry of entries) {
entry.target.style.borderRadius =
Math.max(0, 250 - entry.contentRect.width) + 'px';
}
});
// Наблюдаем только второй блок
ro.observe(document.querySelector('.box:nth-child(2)'));
Еще один интересный пример — окно чата. Проблема, которая возникает в типичном макете беседы, пролистываемой сверху вниз, заключается в расположении прокрутки. Чтобы не вводить пользователя в заблуждение, полезно оставлять окно остается в нижней части беседы, где появляются новые сообщения. Кроме того, любое изменение макета (представьте, что телефон переходит с альбомной ориентации на портретную или наоборот) должно приводить к тому же результату.
ResizeObserver позволяет написать единственный фрагмент кода, который обработает оба сценария. Изменение размера окна — это событие, которое ResizeObserver может захватить по определению, но вызов appendChild() также изменяет размер этого элемента (если не установлено overflow: hidden), потому что ему нужно освободить место для новых элементов. Учитывая это, для достижения желаемого эффекта потребуется всего несколько строк:
const ro = new ResizeObserver(entries => {
document.scrollingElement.scrollTop =
document.scrollingElement.scrollHeight;
});
// Наблюдаем scrollingElement при изменении размеров окна
ro.observe(document.scrollingElement);
// Наблюдаем хронологию для обработки новых сообщений
ro.observe(timeline);
Лаконично, не правда ли?
Далее можно добавить дополнительный код для обработки случая, когда пользователь прокручивает окно вручную и хочет оставить фокус на этом сообщении, когда придет новое.
Еще один вариант использования — любой настраиваемый элемент, имеющий собственный макет. До ResizeObserver не было надежного способа получать уведомления об изменении его размеров, чтобы его дочерние элементы могли быть снова размещены.
Вывод #
ResizeObserver доступен в большинстве основных браузеров. В некоторых из них такая возможность появилась совсем недавно. Доступно несколько вариантов полизаполнения, но полностью дублировать функциональность ResizeObserver невозможно. Текущие реализации полагаются либо на периодическое обновление, либо на добавление контрольных элементов в DOM. Применение первого подхода будет разряжать батарею мобильного телефона, загружая ЦП, а использование второго модифицирует DOM и может испортить стили и другой DOM-зависимый код.
Фото Маркуса Списке c Unsplash.