
Перевод статьи Зелла Лью (Zell Liew) "A Better API for the Resize Observer"
Интерфейсы слежения за изменением размера (Resize Observer), изменениями DOM (Mutation Observer) и пересечениями (Intersection Observer) — это полезные API, которые превосходят своих предшественников по производительности:
- ResizeObserver лучше события
window.resize
. - MutationObserver заменил устаревшие обработчики изменений (Mutation Events).
- IntersectionObserver позволяет реализовать взаимодействие при прокрутке страницы с меньшими расходами по производительности.
API этих трех типов интерфейсов похожи друг на друга (хотя и имеют различия, о которых поговорим позже). Чтобы начать пользоваться этими наблюдателями, необходимо выполнить следующие шаги:
- Создать новый экземпляр наблюдателя с ключевым словом
new
: Этот объект принимает функцию обратного вызова, которая будет выполняться при изменении отслеживаемого элемента. - Реализовать обработку полученных изменений: Это делается через функцию обратного вызова, передаваемую в конструктор наблюдателя.
- Начните наблюдение за определенным элементом: Используем метод
observe
. - (По желанию) прекратить наблюдение за элементом: Используйте методы
unobserve
илиdisconnect
, в зависимости от конкретного наблюдателя.
Например, вот как это работает на практике с ResizeObserver
:
// Шаг 1: Создаем нового наблюдателя
const observer = new ResizeObserver(observerFn);
// Шаг 2: Определяем функцию-обработчик изменений
function observerFn(entries) {
for (let entry of entries) {
// делаем что-то с объектом entry
}
}
// Шаг 3: Начало наблюдения за элементом
const element = document.querySelector("#some-element");
observer.observe(element);
// Шаг 4 (необязательно): Остановка наблюдения
observer.disconnect(element);
Хорошая новость состоит в следующем: я считаю, что API этих интерфейсов слежения можно усовершенствовать и сделать проще в использовании.
Улучшаем ResizeObserver
Начнем с самого простого наблюдателя — ResizeObserver
. Сначала напишем функцию, которая инкапсулирует весь созданный нами resizeObserver.
function resizeObserver() {
// делаем что-нибудь полезное...
}
Самый простой способ начать рефакторинг — поместить весь существующий код внутрь нашей функции resizeObserver
.
function resizeObserver() {
const observer = new ResizeObserver(observerFn);
function observerFn(entries) {
for (let entry of entries) {
// сделать что-нибудь с объектом entry
}
}
const node = document.querySelector("#some-element");
observer.observe(node);
}
Далее можно передать элемент в качестве аргумента функции, чтобы упростить её использование. После этого строка с поиском элемента станет ненужной.
function resizeObserver(element) {
const observer = new ResizeObserver(observerFn);
function observerFn(entries) {
for (let entry of entries) {
// сделать что-нибудь с объектом entry
}
}
observer.observe(element);
}
Теперь наша функция стала универсальной, так как теперь мы можем передать в неё любой элемент.
// Пример использования функции resizeObserver
const node = document.querySelector("#some-element");
const obs = resizeObserver(node);
Это уже гораздо легче, чем каждый раз писать весь код ResizeObserver
заново.
Следующим шагом очевидно будет передача функции обратного вызова (observerFn
) в качестве параметра. Можно поступить так:
// Неплохо, но можно лучше
function resizeObserver(node, observerFn) {
const observer = new ResizeObserver(observerFn);
observer.observe(node);
}
Поскольку функция observerFn
всегда одинакова — она проходит циклом по массиву записей и обрабатывает каждую запись отдельно — мы могли бы сохранить функцию observerFn
и передавать в нее обратный вызов callback
, который выполняется при каждом изменении размера элемента.
// Лучше
function resizeObserver(node, callback) {
const observer = new ResizeObserver(observerFn);
function observerFn(entries) {
for (let entry of entries) {
callback(entry);
}
}
observer.observe(node);
}
Так мы сможем передавать callback
в нашу функцию resizeObserver
. В результате наша функция resizeObserver
будет работать аналогично привычному нам слушателю событий.
// Пример использования функции resizeObserver
const node = document.querySelector("#some-element");
const obs = resizeObserver(node, entry => {
// делаем что-нибудь с каждой записью
});
Можно немного улучшить передачу обратного вызова, предоставляя доступ как к отдельной записи (entry
), так и ко всему списку записей (entries
):
function resizeObserver(element, callback) {
const observer = new ResizeObserver(observerFn);
function observerFn(entries) {
for (let entry of entries) {
callback({ entry, entries });
}
}
observer.observe(element);
}
Теперь мы можем получить доступ к полному списку записей, если это потребуется.
// Пример использования функции resizeObserver
const obs = resizeObserver(node, ({ entry, entries }) => {
// ...
});
Далее разумно преобразовать обратный вызов в необязательный параметр-опцию, чтобы согласовать поведение нашего resizeObserver
с функциями mutationObserver
и intersectionObserver
, которые будут рассмотрены в следующей статье.
function resizeObserver(element, options = {}) {
const { callback } = options;
const observer = new ResizeObserver(observerFn);
function observerFn(entries) {
for (let entry of entries) {
callback({ entry, entries });
}
}
observer.observe(element);
}
Теперь можно использовать функцию resizeObserver
таким образом:
const obs = resizeObserver(node, {
callback({ entry, entries }) {
// делаем что-нибудь...
}
});
Наблюдатель также может принимать дополнительные параметры
Метод observe
класса ResizeObserver
может принимать в качестве параметра объект настроек (options
), который содержит одно свойство — box
. Оно определяет, будет ли наблюдатель фиксировать изменения в границах content-box
, border-box
или device-pixel-content-box
.
Таким образом, нам необходимо извлекать эти настройки из объекта options
и передавать их методу observe
.
function resizeObserver (element, options = {}) {
const { callback, ...opts } = options
// ...
observer.observe(element, opts);
}
Дополнительные возможности: Паттерн прослушивателя событий
Я предпочитаю использовать шаблон обратного вызова callback
, так как он прост и понятен. Но если хочется придерживаться стандартного шаблона прослушивания событий, то можно сделать и это. Проделаем здесь один трюк, чтобы эмитировать событие. Назовём наше событие resize-obs
, поскольку название resize
уже занято.
function resizeObserver(element, options = {}) {
// ...
function observerFn(entries) {
for (let entry of entries) {
if (callback) callback({ entry, entries });
else {
element.dispatchEvent(
new CustomEvent("resize-obs", {
detail: { entry, entries },
})
);
}
}
}
// ...
}
Теперь мы можем прослушивать событие resize-obs
следующим образом:
const obs = resizeObserver(node);
node.addEventListener("resize-obs", (event) => {
const { entry, entries } = event.detail;
});
Это необязательно, но полезно знать.
Останавливаем наблюдение за элементом
Последним шагом является реализация способа остановки наблюдения за элементом, когда это больше не требуется. Для этого мы возвращаем два метода объекта observer
:
unobserve
: прекращает наблюдение за одним элементом.disconnect
: останавливает наблюдение за всеми элементами.
function resizeObserver(node, options = {}) {
// ...
return {
unobserve(node) {
observer.unobserve(node);
},
disconnect() {
observer.disconnect();
},
};
}
В нашем случае оба эти методы работают одинаково, так как мы позволяем нашему resizeObserver
следить только за одним элементом. Поэтому выбирайте тот вариант, который вам больше нравится.
Например:
const obs = resizeObserver(node, {
callback({ entry, entries }) {
// делаем что-нибудь...
},
});
// Прекращаем наблюдение за всеми элементами
obs.disconnect();
Итак, мы завершили создание улучшенного API для ResizeObserver
— функции resizeObserver
.
Итоговый код
Вот написанный нами код для функции resizeObserver
полностью:
export function resizeObserver(node, options = {}) {
const observer = new ResizeObserver(observerFn);
const { callback, ...opts } = options;
function observerFn(entries) {
for (const entry of entries) {
// Шаблон обратного вызова
if (callback) callback({ entry, entries, observer });
// Шаблон прослушивания событий
else {
node.dispatchEvent(
new CustomEvent('resize-obs', {
detail: { entry, entries, observer },
})
);
}
}
}
observer.observe(node);
return {
unobserve(node) {
observer.unobserve(node);
},
disconnect() {
observer.disconnect();
},
};
}
Здесь представлены оба варианта реализации: шаблон обратного вызова и шаблон прослушивания событий.
Практическое применение в библиотеке Splendid Labz
Библиотека утилит Splendid Labz содержит расширенную версию resizeObserver
, которую мы разработали выше. Вы можете использовать её, если предпочитаете более мощный инструмент или не хотите копировать код наблюдателя в свои проекты.
import { resizeObserver } from "@splendidlabz/utils/dom";
const node = document.querySelector(".some-element");
const obs = resizeObserver(node, {
callback({ entry, entries }) {
/* делаете то, что считаете нужным */
},
});
Дополнительно: Версия resizeObserver
от Splendid Labz способна одновременно наблюдать за несколькими элементами. А также снимать наблюдение сразу с нескольких элементов.
const items = document.querySelectorAll(".elements");
const obs = resizeObserver(items, {
callback({ entry, entries }) {
/* делаете то, что считаете нужным */
},
});
// Одновременно снимаем наблюдение с двух элементов
const subset = [items[0], items[1]];
obs.unobserve(subset);
Нашли рефакторинг полезным?
Рефакторинг чрезвычайно полезен (и важен), так как позволяет создать код, который легко использовать и поддерживать.
На этом всё!
Надеюсь, вам понравилась эта статья, и встретимся в следующей части.