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

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

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

Система событий в Leaflet

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

Основные методы:

on()
off()
once()
fire()

Пример подписки на событие:

marker.on('click', function(e) {
    console.log('Маркер нажат');
});

Объекты Leaflet генерируют множество событий:

Событие Описание
click Щелчок мышью
dblclick Двойной щелчок
mousedown Нажатие кнопки мыши
mouseup Отпускание кнопки мыши
mousemove Движение мыши
mouseover Наведение курсора
mouseout Уход курсора
contextmenu Правая кнопка мыши
drag Перетаскивание
zoom Изменение масштаба

При большом количестве объектов назначение обработчика каждому из них может стать неудобным.

Проблема множества обработчиков

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

for (let i = 0; i < 1000; i++) {
    const marker = L.marker([
        randomLat,
        randomLng
    ]).addTo(map);

    marker.on('click', function() {
        console.log('Клик по маркеру');
    });
}

Подход работает корректно, однако обладает рядом недостатков:

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

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

Всплытие событий в Leaflet

Leaflet поддерживает механизм распространения событий вверх по иерархии объектов.

Когда пользователь нажимает на маркер, событие может быть обработано:

  1. самим маркером;
  2. группой слоев;
  3. картой.

Схематично процесс выглядит следующим образом:

Marker
   ↓
LayerGroup
   ↓
FeatureGroup
   ↓
Map

Благодаря этому обработчик может находиться на более высоком уровне.

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

Наиболее распространенный способ делегирования в Leaflet основан на использовании L.FeatureGroup.

Создание группы:

const cities = L.featureGroup().addTo(map);

Добавление маркеров:

const moscow = L.marker([55.7558, 37.6176]);
const spb = L.marker([59.9343, 30.3351]);

cities.addLayer(moscow);
cities.addLayer(spb);

Назначение единственного обработчика:

cities.on('click', function(e) {
    console.log('Нажат объект');
});

Теперь любой объект внутри группы будет вызывать этот обработчик.

Получение объекта-источника

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

Для этого используется свойство:

e.layer

Пример:

cities.on('click', function(e) {
    console.log(e.layer);
});

Leaflet автоматически передает ссылку на объект, инициировавший событие.

Если был нажат маркер:

cities.on('click', function(e) {
    const marker = e.layer;

    console.log(marker.getLatLng());
});

Один обработчик способен обслуживать любое количество элементов.

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

Часто каждому объекту требуется сопоставить дополнительную информацию.

Пример:

const marker = L.marker([55.7558, 37.6176]);

marker.cityInfo = {
    name: 'Москва',
    population: 13000000
};

cities.addLayer(marker);

Делегированный обработчик:

cities.on('click', function(e) {
    console.log(e.layer.cityInfo.name);
});

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

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

Особенно полезным делегирование становится при работе с GeoJSON.

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

const geoLayer = L.geoJSON(data);

Добавление на карту:

geoLayer.addTo(map);

Назначение обработчика:

geoLayer.on('click', function(e) {
    console.log(e.layer.feature.properties);
});

Вне зависимости от количества объектов внутри GeoJSON событие обрабатывается централизованно.

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

Группа слоев также может выступать посредником событий.

Пример:

const group = L.layerGroup().addTo(map);

group.addLayer(
    L.marker([55.7, 37.6])
);

group.addLayer(
    L.marker([59.9, 30.3])
);

Подписка:

group.on('click', function(e) {
    console.log('Сработала группа');
});

Следует учитывать, что FeatureGroup обладает более развитой поддержкой событий и обычно предпочтительнее для подобных задач.

Проверка типа объекта

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

Например:

group.addLayer(
    L.marker([55.7, 37.6])
);

group.addLayer(
    L.circle([55.8, 37.7], {
        radius: 500
    })
);

group.addLayer(
    L.polygon([
        [55.7, 37.5],
        [55.9, 37.6],
        [55.8, 37.8]
    ])
);

Общий обработчик:

group.on('click', function(e) {

    if (e.layer instanceof L.Marker) {
        console.log('Маркер');
    }

    if (e.layer instanceof L.Circle) {
        console.log('Окружность');
    }

    if (e.layer instanceof L.Polygon) {
        console.log('Полигон');
    }

});

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

Динамически создаваемые объекты

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

Сначала создается группа:

const markers = L.featureGroup().addTo(map);

Затем назначается обработчик:

markers.on('click', function(e) {
    console.log('Клик');
});

Позже добавляется новый объект:

const marker = L.marker([
    55.75,
    37.61
]);

markers.addLayer(marker);

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

Новый маркер автоматически участвует в системе событий группы.

Использование addEventParent

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

Добавление родителя:

child.addEventParent(parent);

Пример:

marker.addEventParent(group);

Теперь события маркера смогут передаваться объекту group.

Удаление родителя:

marker.removeEventParent(group);

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

Отключение всплытия событий

Иногда делегирование необходимо ограничить.

Для этого используется:

L.DomEvent.stopPropagation(event);

Либо специальные настройки слоя.

Пример:

marker.on('click', function(e) {
    L.DomEvent.stopPropagation(e);
});

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

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

Карта также может выступать конечным получателем событий.

Пример:

map.on('click', function(e) {
    console.log(e.latlng);
});

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

Например:

map.on('click', function(e) {
    console.log('Клик на карте');
});

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

Централизованная система выбора объектов

Распространенный сценарий — выделение любого объекта по щелчку.

Создание группы:

const selectable = L.featureGroup().addTo(map);

Общий обработчик:

selectable.on('click', function(e) {

    selectable.eachLayer(function(layer) {

        if (layer.setStyle) {
            layer.setStyle({
                color: '#3388ff'
            });
        }

    });

    if (e.layer.setStyle) {
        e.layer.setStyle({
            color: 'red'
        });
    }

});

Независимо от количества элементов логика выбора сосредоточена в одном месте.

Делегирование и производительность

При работе с большим объемом данных преимущества становятся особенно заметны.

Сравнение подходов:

Индивидуальные обработчики

marker.on('click', handler);

Для каждой сущности создается собственная подписка.

Делегированный обработчик

group.on('click', handler);

Для всей коллекции существует единая точка обработки.

Преимущества:

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

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

Создание интерактивного слоя городов:

const cities = L.featureGroup().addTo(map);

const data = [
    {
        name: 'Москва',
        coords: [55.7558, 37.6176]
    },
    {
        name: 'Санкт-Петербург',
        coords: [59.9343, 30.3351]
    },
    {
        name: 'Казань',
        coords: [55.7961, 49.1064]
    }
];

Добавление объектов:

data.forEach(city => {

    const marker = L.marker(city.coords);

    marker.cityName = city.name;

    cities.addLayer(marker);

});

Делегированный обработчик:

cities.on('click', function(e) {

    L.popup()
        .setLatLng(
            e.layer.getLatLng()
        )
        .setContent(
            e.layer.cityName
        )
        .openOn(map);

});

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

Рекомендации по использованию

Использовать FeatureGroup для интерактивных наборов объектов.

const group = L.featureGroup();

Хранить общую логику обработки в одном месте.

group.on('click', handler);

Передавать пользовательские данные через свойства объекта или GeoJSON-свойства.

marker.metadata = {...};

Проверять тип слоя перед выполнением специфических операций.

if (e.layer instanceof L.Marker) {
    ...
}

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

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