В основе работы Popper.js лежит система модификаторов — небольших, изолированных функций, каждая из которых отвечает за конкретный аспект позиционирования: вычисление смещения, предотвращение выхода за границы, добавление стрелки и т.д. Композиция модификаторов представляет собой механизм, позволяющий объединять эти функции в последовательную цепочку обработки данных.
Каждый модификатор получает текущее состояние (state),
изменяет его и передаёт дальше. Итоговое позиционирование формируется
как результат последовательного применения всех модификаторов.
Модификатор — это объект с чётко определённой структурой:
const myModifier = {
name: 'myModifier',
enabled: true,
phase: 'main',
fn({ state, options, name, instance }) {
// логика модификатора
},
effect({ state, options, name, instance }) {
// побочный эффект (опционально)
},
requires: ['otherModifier'],
options: {}
};
Ключевые свойства:
name — уникальное имя модификатораenabled — флаг активацииphase — этап выполненияfn — основная функцияeffect — функция для побочных эффектовrequires — зависимости от других модификаторовoptions — пользовательские настройкиКомпозиция строится не только на порядке в массиве, но и на фазах. Popper.js использует фиксированный набор фаз:
Каждая фаза предназначена для определённого типа операций:
Такое разделение предотвращает лишние перерисовки и улучшает производительность.
Модификаторы сортируются по фазам и зависимостям. Итоговый порядок определяется следующим образом:
requires)Пример:
createPopper(reference, popper, {
modifiers: [
{ name: 'offset', options: { offset: [0, 8] } },
{ name: 'preventOverflow' },
{ name: 'flip' }
]
});
Даже если порядок в массиве изменится, Popper.js выстроит корректную цепочку выполнения.
Объект state — центральный элемент композиции. Он
содержит:
state = {
placement: 'bottom',
rects: {
reference: DOMRect,
popper: DOMRect
},
modifiersData: {},
styles: {},
elements: {
reference,
popper
}
};
Каждый модификатор может:
statestate.modifiersDatastate.stylesПример взаимодействия:
fn({ state }) {
state.modifiersData.myModifier = {
x: 10,
y: 20
};
}
Другой модификатор может использовать эти данные:
fn({ state }) {
const data = state.modifiersData.myModifier;
}
Свойство requires позволяет явно указать
зависимости:
{
name: 'customModifier',
requires: ['offset', 'popperOffsets'],
fn({ state }) {
// гарантировано, что offset уже выполнен
}
}
Также существует requiresIfExists, которое не вызывает
ошибку, если зависимость отсутствует:
requiresIfExists: ['arrow']
Это делает систему гибкой и расширяемой.
Функция effect используется для операций, которые не
связаны напрямую с вычислениями:
effect({ state }) {
const observer = new ResizeObserver(() => {
// обновление позиции
});
observer.observe(state.elements.reference);
return () => {
observer.disconnect();
};
}
Особенности:
Модификаторы можно настраивать или переопределять:
const customOffset = {
name: 'offset',
options: {
offset: [0, 20]
}
};
Или полностью заменить поведение:
const overrideFlip = {
name: 'flip',
fn({ state }) {
// собственная логика flip
}
};
Композиция позволяет добавлять собственные модификаторы в цепочку:
const logModifier = {
name: 'log',
phase: 'main',
fn({ state }) {
console.log(state.placement);
}
};
createPopper(reference, popper, {
modifiers: [
logModifier,
{ name: 'offset' },
{ name: 'preventOverflow' }
]
});
Важно учитывать:
stateХотя Popper автоматически сортирует модификаторы, порядок можно контролировать через:
phaserequiresrequiresIfExistsПример тонкой настройки:
const first = {
name: 'first',
phase: 'beforeMain',
fn: () => {}
};
const second = {
name: 'second',
requires: ['first'],
fn: () => {}
};
Эффективная композиция модификаторов требует:
Минимизации работы в фазах write
Разделения ответственности
Повторного использования данных
modifiersDataКонтроля зависимостей
1. Нарушение фазовой логики
Чтение DOM в write фазе:
phase: 'write' // ошибка для чтения
2. Отсутствие зависимостей
// используется offset, но не указан requires
3. Перезапись state без учета других модификаторов
state.styles = {} // уничтожает данные
4. Избыточные вычисления
Повторные расчёты вместо использования
modifiersData.
Встроенные модификаторы образуют сложную цепочку:
eventListenerspopperOffsetscomputeStylesapplyStylesflippreventOverflowoffsetarrowКаждый из них взаимодействует через state, образуя
единый конвейер обработки.
Popper позволяет изменять модификаторы во время работы:
instance.setOptions({
modifiers: [
{ name: 'offset', options: { offset: [0, 50] } }
]
});
Это приводит к:
effectКомпозицию модификаторов удобно воспринимать как:
Такой подход делает Popper.js:
createPopper(reference, popper, {
modifiers: [
{
name: 'offset',
options: { offset: [0, 10] }
},
{
name: 'preventOverflow',
options: { boundary: 'viewport' }
},
{
name: 'flip',
options: { fallbackPlacements: ['top', 'right'] }
},
{
name: 'customLogger',
phase: 'afterMain',
fn({ state }) {
console.log('Final placement:', state.placement);
}
}
]
});
В этом примере:
offset задаёт смещениеpreventOverflow ограничивает выход за границыflip меняет сторону при необходимостиcustomLogger получает итоговое состояниеКомпозиция модификаторов позволяет:
Каждый модификатор — это независимый строительный блок, а их комбинация формирует итоговое поведение системы позиционирования.