Композиция модификаторов

В основе работы 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 использует фиксированный набор фаз:

  1. beforeRead
  2. read
  3. afterRead
  4. beforeMain
  5. main
  6. afterMain
  7. beforeWrite
  8. write
  9. afterWrite

Каждая фаза предназначена для определённого типа операций:

  • read — чтение данных из DOM
  • main — вычисления
  • write — применение изменений к DOM

Такое разделение предотвращает лишние перерисовки и улучшает производительность.


Последовательность выполнения

Модификаторы сортируются по фазам и зависимостям. Итоговый порядок определяется следующим образом:

  1. Группировка по фазам
  2. Учет зависимостей (requires)
  3. Стабильная сортировка внутри фаз

Пример:

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
  }
};

Каждый модификатор может:

  • читать данные из state
  • записывать результаты в state.modifiersData
  • изменять state.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']

Это делает систему гибкой и расширяемой.


Побочные эффекты и lifecycle

Функция 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 автоматически сортирует модификаторы, порядок можно контролировать через:

  • phase
  • requires
  • requiresIfExists

Пример тонкой настройки:

const first = {
  name: 'first',
  phase: 'beforeMain',
  fn: () => {}
};

const second = {
  name: 'second',
  requires: ['first'],
  fn: () => {}
};

Оптимизация композиции

Эффективная композиция модификаторов требует:

Минимизации работы в фазах write

  • избегание лишних изменений DOM

Разделения ответственности

  • один модификатор — одна задача

Повторного использования данных

  • хранение промежуточных результатов в modifiersData

Контроля зависимостей

  • исключение скрытых связей

Типичные ошибки

1. Нарушение фазовой логики

Чтение DOM в write фазе:

phase: 'write' // ошибка для чтения

2. Отсутствие зависимостей

// используется offset, но не указан requires

3. Перезапись state без учета других модификаторов

state.styles = {} // уничтожает данные

4. Избыточные вычисления

Повторные расчёты вместо использования modifiersData.


Внутренняя композиция Popper.js

Встроенные модификаторы образуют сложную цепочку:

  • eventListeners
  • popperOffsets
  • computeStyles
  • applyStyles
  • flip
  • preventOverflow
  • offset
  • arrow

Каждый из них взаимодействует через state, образуя единый конвейер обработки.


Динамическая композиция

Popper позволяет изменять модификаторы во время работы:

instance.setOptions({
  modifiers: [
    { name: 'offset', options: { offset: [0, 50] } }
  ]
});

Это приводит к:

  • пересортировке модификаторов
  • повторному выполнению effect
  • обновлению позиции

Ментальная модель композиции

Композицию модификаторов удобно воспринимать как:

  • pipeline (конвейер) — данные проходят через этапы
  • middleware — каждый модификатор дополняет состояние
  • граф зависимостей — порядок определяется связями

Такой подход делает 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 получает итоговое состояние

Гибкость композиции

Композиция модификаторов позволяет:

  • создавать собственные стратегии позиционирования
  • переопределять стандартное поведение
  • интегрироваться с внешними системами
  • управлять производительностью

Каждый модификатор — это независимый строительный блок, а их комбинация формирует итоговое поведение системы позиционирования.