Система событий в Mapbox GL JS построена вокруг механизма подписки на
события объектов карты, источников данных, слоёв и элементов интерфейса.
Практически любое действие пользователя или изменение состояния карты
генерирует событие, которое может быть обработано через методы
on(), off() и once().
При работе с интерактивными картами количество объектов и слоёв часто оказывается очень большим. Привязка отдельных обработчиков к каждому объекту приводит к усложнению кода и дополнительным накладным расходам. Для решения этой проблемы применяется делегирование событий (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]);
});
Несмотря на то что слой может содержать тысячи объектов, обработчик остаётся единственным.
Метод делегирования имеет следующую форму:
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);
Событие наведения мыши является одной из самых частых задач.
Срабатывает при входе курсора в объект слоя.
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 feature = e.features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(`
<h3>${feature.properties.name}</h3>
<p>${feature.properties.population}</p>
`)
.addTo(map);
});
Обработчик автоматически работает для любого объекта слоя.
Обработка двойного щелчка:
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);
});
Часто делегирование используется совместно с механизмом состояний объектов.
Наведение на объект:
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');
});
После первого срабатывания подписка автоматически снимается.
Иногда требуется более гибкое управление.
Подписка на карту:
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);
Изменения в поведении требуют редактирования только одной функции.
Ошибка:
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.