Event delegation

Система событий в Mapbox GL JS построена вокруг механизма подписки на события объектов карты, источников данных, слоёв и элементов интерфейса. Практически любое действие пользователя или изменение состояния карты генерирует событие, которое может быть обработано через методы on(), off() и once().

При работе с интерактивными картами количество объектов и слоёв часто оказывается очень большим. Привязка отдельных обработчиков к каждому объекту приводит к усложнению кода и дополнительным накладным расходам. Для решения этой проблемы применяется делегирование событий (Event Delegation).


Что такое Event Delegation

Делегирование событий — это подход, при котором обработчик назначается не каждому объекту отдельно, а одному общему контейнеру. После возникновения события определяется конкретный элемент, ставший его источником.

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

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

marker1.getElement().addEventListener('click', handler);
marker2.getElement().addEventListener('click', handler);
marker3.getElement().addEventListener('click', handler);

используется единый обработчик:

map.on('click', 'cities-layer', handler);

В результате любое нажатие на объект слоя будет обрабатываться одной функцией.


Делегирование через слой

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

Рассмотрим GeoJSON-источник:

map.addSource('cities', {
    type: 'geojson',
    data: 'cities.geojson'
});

Создание слоя:

map.addLayer({
    id: 'cities-layer',
    type: 'circle',
    source: 'cities',
    paint: {
        'circle-radius': 8,
        'circle-color': '#ff0000'
    }
});

Теперь можно подписаться на клики по любому объекту слоя:

map.on('click', 'cities-layer', (event) => {
    console.log(event.features[0]);
});

Несмотря на то что слой может содержать тысячи объектов, обработчик остаётся единственным.


Сигнатура метода on()

Метод делегирования имеет следующую форму:

map.on(eventType, layerId, callback);

Параметры:

Параметр Описание
eventType Тип события
layerId Идентификатор слоя
callback Функция обработки

Пример:

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

При возникновении события Mapbox автоматически определяет объекты слоя под курсором и передаёт их через массив features.


Объект события

При делегировании в обработчик передаётся специальный объект события.

Пример:

map.on('click', 'cities-layer', (e) => {
    console.log(e);
});

Основные свойства:

Координаты курсора

e.point

Пример:

console.log(e.point.x);
console.log(e.point.y);

Географические координаты

e.lngLat

Пример:

console.log(e.lngLat.lng);
console.log(e.lngLat.lat);

Найденные объекты

e.features

Пример:

const feature = e.features[0];

console.log(feature.properties.name);

Делегирование событий наведения

Событие наведения мыши является одной из самых частых задач.

mouseenter

Срабатывает при входе курсора в объект слоя.

map.on('mouseenter', 'cities-layer', () => {
    map.getCanvas().style.cursor = 'pointer';
});

mouseleave

Срабатывает при выходе курсора за пределы объекта.

map.on('mouseleave', 'cities-layer', () => {
    map.getCanvas().style.cursor = '';
});

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


Делегирование события click

Наиболее популярный сценарий — открытие всплывающего окна.

map.on('click', 'cities-layer', (e) => {
    const feature = e.features[0];

    new mapboxgl.Popup()
        .setLngLat(feature.geometry.coordinates)
        .setHTML(`
            <h3>${feature.properties.name}</h3>
            <p>${feature.properties.population}</p>
        `)
        .addTo(map);
});

Обработчик автоматически работает для любого объекта слоя.


Делегирование события dblclick

Обработка двойного щелчка:

map.on('dblclick', 'cities-layer', (e) => {
    console.log('Double click');
});

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


Делегирование событий контекстного меню

Событие правой кнопки мыши:

map.on('contextmenu', 'cities-layer', (e) => {
    console.log('Context menu');
});

Пример получения объекта:

map.on('contextmenu', 'cities-layer', (e) => {
    const city = e.features[0];

    console.log(city.properties.name);
});

Делегирование для полигонов

Принцип работы одинаков для любых типов геометрии.

Создание полигона:

map.addLayer({
    id: 'regions',
    type: 'fill',
    source: 'regions-source',
    paint: {
        'fill-color': '#0080ff',
        'fill-opacity': 0.4
    }
});

Подписка:

map.on('click', 'regions', (e) => {
    console.log(e.features[0].properties.region_name);
});

Делегирование для линий

Работа с линейными объектами:

map.addLayer({
    id: 'roads',
    type: 'line',
    source: 'roads-source',
    paint: {
        'line-width': 4,
        'line-color': '#ff6600'
    }
});

Обработчик:

map.on('mouseenter', 'roads', () => {
    console.log('Road hovered');
});

Работа с несколькими слоями

Иногда требуется одинаковая логика для нескольких слоёв.

Неэффективный вариант:

map.on('click', 'cities', cityHandler);
map.on('click', 'villages', cityHandler);
map.on('click', 'capitals', cityHandler);

Более удобный подход:

const layers = [
    'cities',
    'villages',
    'capitals'
];

layers.forEach(layer => {
    map.on('click', layer, cityHandler);
});

Обработчик остаётся единым.


Использование свойств объектов

Делегирование особенно полезно при работе с атрибутами GeoJSON.

Пример структуры данных:

{
  "type": "Feature",
  "properties": {
    "id": 15,
    "name": "Almaty",
    "category": "city"
  }
}

Получение данных:

map.on('click', 'cities-layer', (e) => {
    const feature = e.features[0];

    console.log(feature.properties.id);
    console.log(feature.properties.name);
    console.log(feature.properties.category);
});

Делегирование и Feature State

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

Наведение на объект:

let hoveredId = null;

map.on('mousemove', 'cities-layer', (e) => {
    if (hoveredId !== null) {
        map.setFeatureState(
            {
                source: 'cities',
                id: hoveredId
            },
            {
                hover: false
            }
        );
    }

    hoveredId = e.features[0].id;

    map.setFeatureState(
        {
            source: 'cities',
            id: hoveredId
        },
        {
            hover: true
        }
    );
});

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

'circle-radius': [
    'case',
    ['boolean', ['feature-state', 'hover'], false],
    12,
    6
]

В результате объект увеличивается при наведении курсора.


Удаление делегированных обработчиков

Для удаления подписки применяется метод off().

Пример:

function handleClick(e) {
    console.log(e.features[0]);
}

map.on('click', 'cities-layer', handleClick);

Удаление:

map.off('click', 'cities-layer', handleClick);

Важно использовать ту же ссылку на функцию.

Неверный вариант:

map.off('click', 'cities-layer', () => {});

В этом случае удаление не произойдёт.


Одноразовые обработчики

Метод once() выполняет обработчик только один раз.

map.once('click', 'cities-layer', (e) => {
    console.log('First click only');
});

После первого срабатывания подписка автоматически снимается.


Делегирование через queryRenderedFeatures()

Иногда требуется более гибкое управление.

Подписка на карту:

map.on('click', (e) => {
    const features = map.queryRenderedFeatures(
        e.point,
        {
            layers: ['cities-layer']
        }
    );

    if (!features.length) {
        return;
    }

    console.log(features[0]);
});

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


Делегирование для динамически обновляемых данных

Одно из важнейших преимуществ делегирования заключается в поддержке динамического контента.

Допустим, источник данных регулярно обновляется:

source.setData(newGeoJson);

Количество объектов может изменяться:

  • добавление новых точек;
  • удаление существующих точек;
  • изменение координат;
  • изменение атрибутов.

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

map.on('click', 'cities-layer', handler);

Повторная регистрация событий не требуется.


Производительность делегирования

При наличии большого количества объектов делегирование обеспечивает несколько преимуществ:

Меньше обработчиков

Один слой:

map.on('click', 'cities-layer', handler);

вместо тысяч индивидуальных подписок.


Меньше потребление памяти

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

Единый обработчик значительно сокращает объём используемой памяти.


Упрощение сопровождения

Вся логика взаимодействия сосредоточена в одном месте:

function handleCityClick(e) {
    const city = e.features[0];

    showCityInfo(city);
}

map.on('click', 'cities-layer', handleCityClick);

Изменения в поведении требуют редактирования только одной функции.


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

Работа с пустым массивом features

Ошибка:

const feature = e.features[0];

Без проверки возможна ситуация отсутствия объектов.

Безопасный вариант:

if (!e.features || !e.features.length) {
    return;
}

const feature = e.features[0];

Удаление слоя без снятия обработчиков

Нежелательный сценарий:

map.removeLayer('cities-layer');

При сложной архитектуре рекомендуется сначала удалить подписки:

map.off('click', 'cities-layer', handleClick);

map.removeLayer('cities-layer');

Избыточное количество одинаковых подписок

Проблемный код:

map.on('click', 'cities-layer', handler);
map.on('click', 'cities-layer', handler);
map.on('click', 'cities-layer', handler);

Каждый клик вызовет обработчик несколько раз.

Следует контролировать жизненный цикл подписок и избегать повторной регистрации.


Практический пример

Интерактивный слой населённых пунктов:

map.on('mouseenter', 'cities-layer', () => {
    map.getCanvas().style.cursor = 'pointer';
});

map.on('mouseleave', 'cities-layer', () => {
    map.getCanvas().style.cursor = '';
});

map.on('click', 'cities-layer', (e) => {
    const city = e.features[0];

    new mapboxgl.Popup()
        .setLngLat(city.geometry.coordinates)
        .setHTML(`
            <strong>${city.properties.name}</strong>
        `)
        .addTo(map);
});

В данном примере один слой получает полный набор интерактивного поведения:

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

Именно поэтому делегирование событий считается базовым и наиболее эффективным способом организации взаимодействия с географическими объектами в Mapbox GL JS.