Приоритет событий

Mapbox GL JS представляет собой событийно-ориентированную библиотеку. Практически любое действие пользователя, изменение состояния карты, загрузка ресурсов или взаимодействие со слоями генерирует события. В сложных приложениях одновременно могут происходить десятки событий, поэтому понимание их приоритета и порядка выполнения становится важной частью архитектуры интерфейса.

Под приоритетом событий обычно понимается порядок их возникновения и обработки в рамках жизненного цикла карты. Хотя библиотека не предоставляет механизма явного назначения приоритетов обработчикам, последовательность вызова событий подчиняется строгим правилам внутреннего движка.


Источник событий: Evented

Большинство объектов Mapbox GL JS наследуются от базового класса Evented.

К таким объектам относятся:

  • Map
  • Marker
  • Popup
  • GeolocationControl
  • пользовательские контролы

Подписка выполняется через метод:

map.on('click', () => {
    console.log('Клик по карте');
});

Удаление обработчика:

function handleClick() {
    console.log('Клик');
}

map.on('click', handleClick);
map.off('click', handleClick);

Однократная подписка:

map.once('load', () => {
    console.log('Карта загружена');
});

При анализе приоритетов необходимо помнить, что порядок выполнения начинается именно с механизма Evented.


Последовательность регистрации обработчиков

Если несколько обработчиков подписаны на одно событие, они выполняются в порядке регистрации.

Пример:

map.on('click', () => {
    console.log('Первый');
});

map.on('click', () => {
    console.log('Второй');
});

map.on('click', () => {
    console.log('Третий');
});

Результат:

Первый
Второй
Третий

Таким образом, среди обработчиков одного события наивысший приоритет имеет обработчик, зарегистрированный раньше остальных.


Жизненный цикл загрузки карты

Одной из наиболее важных последовательностей является цикл инициализации карты.

Создание экземпляра:

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [37.6173, 55.7558],
    zoom: 10
});

После этого начинают возникать различные события.

Упрощённый порядок:

style.load
dataloading
sourcedataloading
data
sourcedata
load
idle

Каждое следующее событие появляется только после выполнения определённого этапа загрузки.


Приоритет события style.load

Событие style.load возникает после полной загрузки стиля карты.

map.on('style.load', () => {
    console.log('Стиль загружен');
});

Именно на этом этапе безопасно:

  • добавлять собственные источники данных;
  • создавать пользовательские слои;
  • изменять параметры отображения.

Пример:

map.on('style.load', () => {
    map.addSource('cities', {
        type: 'geojson',
        data: 'cities.geojson'
    });
});

Если попытаться добавить источник раньше, возникнет ошибка.

Следовательно, style.load имеет более высокий фактический приоритет по сравнению с большинством операций над слоями.


Приоритет события load

Событие load считается одним из главных событий жизненного цикла карты.

map.on('load', () => {
    console.log('Карта готова');
});

Оно возникает после:

  • загрузки стиля;
  • инициализации источников;
  • подготовки первоначального отображения.

Типичная последовательность:

style.load
↓
load
↓
idle

Поэтому код, которому требуется полностью работоспособная карта, обычно размещается именно здесь.


Событие idle как завершающий этап

idle возникает тогда, когда карта больше не выполняет фоновых операций.

map.on('idle', () => {
    console.log('Все данные загружены');
});

К моменту его возникновения:

  • завершены запросы к тайлам;
  • завершён рендеринг;
  • отсутствуют анимации;
  • отсутствуют ожидающие обновления стиля.

Порядок:

load
↓
render
↓
idle

Поэтому idle фактически обладает самым низким приоритетом среди событий загрузки, но предоставляет наиболее полную информацию о готовности карты.


Приоритет событий данных

Mapbox GL JS активно использует события загрузки данных.

Основные из них:

dataloading
data

sourcedataloading
sourcedata

styledataloading
styledata

Последовательность выглядит следующим образом:

styledataloading
↓
styledata
↓
sourcedataloading
↓
sourcedata
↓
load

Пример отслеживания:

map.on('sourcedataloading', () => {
    console.log('Источник загружается');
});

map.on('sourcedata', () => {
    console.log('Источник загружен');
});

События загрузки всегда предшествуют соответствующим событиям завершения.


Приоритет пользовательских событий мыши

Для одного действия пользователя может генерироваться несколько событий.

Например:

mousedown
mouseup
click

Последовательность возникновения:

map.on('mousedown', () => {
    console.log('Нажатие');
});

map.on('mouseup', () => {
    console.log('Отпускание');
});

map.on('click', () => {
    console.log('Клик');
});

Результат:

Нажатие
Отпускание
Клик

Следовательно:

mousedown > mouseup > click

Здесь знак > означает более раннее возникновение события.


Приоритет двойного клика

Двойной щелчок формируется из нескольких событий.

Типичный порядок:

mousedown
mouseup
click

mousedown
mouseup
click

dblclick

Пример:

map.on('click', () => {
    console.log('click');
});

map.on('dblclick', () => {
    console.log('dblclick');
});

Событие dblclick всегда возникает позже обычного клика.


Приоритет событий перемещения мыши

Во время движения курсора возникают:

mouseenter
mousemove
mouseleave

Порядок выглядит так:

mouseenter
↓
mousemove
↓
mouseleave

Пример:

map.on('mouseenter', 'buildings', () => {
    console.log('Вход');
});

map.on('mousemove', 'buildings', () => {
    console.log('Движение');
});

map.on('mouseleave', 'buildings', () => {
    console.log('Выход');
});

Приоритет событий касания

На мобильных устройствах используется отдельная группа событий.

Основные:

touchstart
touchmove
touchend

Пример:

map.on('touchstart', () => {
    console.log('Начало касания');
});

map.on('touchmove', () => {
    console.log('Перемещение');
});

map.on('touchend', () => {
    console.log('Завершение');
});

Порядок строго соответствует последовательности действий пользователя.


Приоритет событий перемещения карты

Когда пользователь начинает перемещать карту, возникает целая цепочка событий.

Упрощённый порядок:

movestart
move
move
move
...
moveend

Пример:

map.on('movestart', () => {
    console.log('Начало движения');
});

map.on('move', () => {
    console.log('Движение');
});

map.on('moveend', () => {
    console.log('Завершение движения');
});

Приоритет:

movestart
↓
move
↓
moveend

Вложенные события и их влияние на порядок

Обработчик одного события способен инициировать другое событие.

Пример:

map.on('click', () => {
    map.fire('custom');
});

map.on('custom', () => {
    console.log('Пользовательское событие');
});

При клике:

click
↓
custom

Внутренний вызов fire() создаёт вложенную цепочку выполнения.


Приоритет событий масштабирования

Во время изменения масштаба используются:

zoomstart
zoom
zoomend

Пример:

map.on('zoomstart', () => {
    console.log('Начало');
});

map.on('zoom', () => {
    console.log('Изменение');
});

map.on('zoomend', () => {
    console.log('Конец');
});

Последовательность:

zoomstart
↓
zoom
↓
zoomend

Комбинация move и zoom

Изменение масштаба часто сопровождается изменением положения карты.

В результате возникает сложная цепочка:

movestart
zoomstart

move
zoom

move
zoom

zoomend
moveend

Точный порядок зависит от типа взаимодействия и текущего состояния карты.

Поэтому в производственных проектах рекомендуется не предполагать жёсткую последовательность между событиями разных категорий.


События вращения карты

Для вращения используются:

rotatestart
rotate
rotateend

Пример:

map.on('rotatestart', () => {
    console.log('Старт');
});

map.on('rotate', () => {
    console.log('Поворот');
});

map.on('rotateend', () => {
    console.log('Конец');
});

Иерархия полностью аналогична масштабированию и перемещению.


События изменения наклона

При работе с трёхмерными картами:

pitchstart
pitch
pitchend

Пример:

map.on('pitchstart', () => {
    console.log('Начало наклона');
});

Далее выполняются события pitch, а завершает цикл pitchend.


Порядок событий рендеринга

Во время каждого кадра вызывается событие:

map.on('render', () => {
    console.log('Кадр отрисован');
});

Особенности:

  • возникает очень часто;
  • может вызываться десятки раз в секунду;
  • выполняется раньше idle.

Типичная последовательность:

load
↓
render
↓
render
↓
render
↓
idle

Из-за высокой частоты вызовов обработчики render должны быть максимально лёгкими.


Приоритет событий слоёв

Mapbox GL JS позволяет подписываться на события конкретного слоя.

Пример:

map.on('click', 'buildings', (e) => {
    console.log('Клик по зданию');
});

Одновременно может существовать общий обработчик:

map.on('click', () => {
    console.log('Клик по карте');
});

При попадании курсора в объект слоя сначала выполняется обработчик слоя, затем общий обработчик карты.

Схема:

Layer Click
↓
Map Click

Это позволяет реализовывать локальную обработку взаимодействия без потери глобальной логики приложения.


Конкуренция событий в сложных интерфейсах

В больших проектах одновременно могут работать:

mousemove
render
move
zoom
sourcedata
idle

Для контроля порядка обычно применяются:

  • флаги состояния;
  • очереди задач;
  • механизмы debounce;
  • механизмы throttle;
  • пользовательские события.

Пример контроля состояния:

let loading = true;

map.on('idle', () => {
    loading = false;
});

map.on('click', () => {
    if (loading) {
        return;
    }

    console.log('Обработка клика');
});

Такой подход позволяет искусственно создавать логические приоритеты поверх стандартной системы событий библиотеки.


Практическая схема приоритетов

Обобщённая последовательность наиболее важных групп событий выглядит следующим образом:

1. style.load

2. dataloading
   sourcedataloading
   styledataloading

3. data
   sourcedata
   styledata

4. load

5. render

6. Пользовательские события:
   mousedown
   mouseup
   click
   dblclick

7. События взаимодействия:
   movestart
   move
   moveend

   zoomstart
   zoom
   zoomend

   rotatestart
   rotate
   rotateend

   pitchstart
   pitch
   pitchend

8. idle

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