Evented интерфейс

Evented — базовый механизм событийной модели, на котором строится взаимодействие между компонентами MapLibre GL JS. Практически все ключевые сущности библиотеки (карта, источники данных, слои, стили) наследуют или используют Evented-интерфейс для организации реактивного поведения.

Evented реализует классическую модель publish/subscribe, где объект:

  • публикует события (emit / fire),
  • позволяет подписываться на них (on),
  • поддерживает одноразовые обработчики (once),
  • управляет жизненным циклом подписок (off / removeListener).

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

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

on — подписка на событие

Метод on регистрирует обработчик события:

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

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

Пример с пространством имён:

map.on('click.userLayer', 'my-layer', (e) => {
    console.log('Клик по слою my-layer');
});

Здесь:

  • click — тип события,
  • userLayer — namespace,
  • 'my-layer' — фильтр по слою.

off — удаление обработчиков

Метод off используется для удаления ранее зарегистрированных обработчиков:

function handler() {
    console.log('click');
}

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

Удаление возможно по:

  • типу события,
  • функции-обработчику,
  • namespace,
  • комбинации параметров.

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


once — одноразовые события

Метод once позволяет зарегистрировать обработчик, который выполнится только один раз:

map.once('idle', () => {
    console.log('карта завершила все рендеры');
});

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


fire — генерация события

Метод fire используется для генерации пользовательских событий:

map.fire('custom-event', {
    detail: { value: 42 }
});

Событие передаётся всем подписанным слушателям.


Структура объекта события

Каждое событие передаёт объект event, содержащий стандартные поля:

  • type — тип события,
  • target — объект-источник,
  • originalEvent — нативное событие (если есть),
  • дополнительные поля, специфичные для события.

Пример обработки:

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

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

  • координаты lngLat,
  • пиксельные координаты point,
  • массив объектов features при queryRenderedFeatures.

Наследование Evented

Evented является базовым классом, который расширяется множеством компонентов библиотеки:

  • Map
  • Style
  • Source
  • Layer
  • Control

Это обеспечивает единый интерфейс подписки на события независимо от уровня абстракции.

Пример:

const source = map.getSource('points');

source.on('data', (e) => {
    console.log('Данные источника обновились');
});

Контекст выполнения обработчиков

Обработчики событий выполняются с контекстом, зависящим от реализации, но обычно:

  • this указывает на объект-источник события,
  • рекомендуется использовать стрелочные функции для избежания неоднозначности контекста.
map.on('load', function () {
    console.log(this === map); // true
});

Пространства имён событий

Evented поддерживает namespace-модель для управления группами обработчиков:

map.on('click.layerA', handlerA);
map.on('click.layerB', handlerB);

Удаление по namespace:

map.off('click.layerA');

Это позволяет:

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

Внутренний механизм хранения подписок

Evented хранит обработчики в структуре, аналогичной словарю:

eventType -> [handlers]
eventType.namespace -> [handlers]

Каждый обработчик хранит:

  • ссылку на функцию,
  • контекст,
  • фильтры (если есть),
  • метаданные namespace.

При вызове события происходит:

  1. поиск всех подписчиков по типу,
  2. фильтрация по namespace,
  3. последовательный вызов обработчиков,
  4. обработка исключений внутри каждого вызова.

Вложенные события и цепочки вызовов

Evented допускает каскадную генерацию событий:

map.on('data', () => {
    map.fire('data-processed');
});

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


Обработка ошибок в событиях

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

  • каждый обработчик выполняется в отдельном try/catch,
  • ошибки логируются,
  • остальные подписчики продолжают работу.
map.on('click', () => {
    throw new Error('test');
});

Другие обработчики click при этом продолжают выполняться.


Практика использования в интерфейсах

Evented широко применяется для построения UI-слоёв поверх карты:

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

Пример синхронизации:

map.on('move', () => {
    const center = map.getCenter();
    store.set('center', center);
});

Удаление всех подписок

В некоторых реализациях доступен метод очистки всех слушателей:

map.remove();

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


Типизация событий (TypeScript-аспект)

В TypeScript-окружении Evented позволяет описывать типы событий:

map.on('click', (e: mapboxgl.MapMouseEvent) => {
    console.log(e.lngLat);
});

Это улучшает:

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

Роль Evented в общей системе MapLibre

Evented выступает связующим слоем между:

  • рендерингом WebGL,
  • источниками данных (GeoJSON, vector tiles),
  • пользовательским вводом,
  • стилевой системой.

Без Evented архитектура библиотеки теряет реактивность и становится набором изолированных компонентов без синхронизации состояния.