Проблемы с событиями

Система событий в MapLibre GL JS является основой взаимодействия между приложением и картой. Через события реализуются реакции на действия пользователя, загрузку данных, изменения состояния карты, работу источников данных и слоёв. Однако именно события часто становятся источником трудноуловимых ошибок: обработчики не вызываются, срабатывают несколько раз, работают в неправильном порядке или приводят к утечкам памяти.

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


Устройство системы событий

Объект карты предоставляет методы регистрации обработчиков:

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

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

map.off('click', handler);

Однократное выполнение:

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

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


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

Одна из самых распространённых ситуаций — зарегистрированный обработчик никогда не срабатывает.

Пример:

map.on('click', () => {
    console.log('Событие произошло');
});

Если сообщений в консоли нет, необходимо проверить несколько факторов.

Неверное имя события

MapLibre строго проверяет названия событий.

Ошибка:

map.on('clicked', () => {
    console.log('Ошибка');
});

Правильно:

map.on('click', () => {
    console.log('Работает');
});

Даже небольшая опечатка полностью отключает обработчик.


Объект карты ещё не создан

Ошибка возникает при попытке регистрации событий до создания экземпляра карты.

Неправильно:

let map;

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

Правильно:

const map = new maplibregl.Map({
    container: 'map',
    style: 'style.json'
});

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

Обработчик зарегистрирован слишком поздно

Некоторые события возникают только один раз.

Например:

map.on('load', () => {
    console.log('Загрузка завершена');
});

Если регистрация происходит после загрузки карты:

setTimeout(() => {
    map.on('load', () => {
        console.log('Не вызовется');
    });
}, 5000);

Событие уже произошло и больше не повторится.

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

if (map.loaded()) {
    console.log('Карта уже загружена');
}

Событие вызывается несколько раз

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

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

Пример:

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

Если функция вызывается несколько раз:

initialize();
initialize();
initialize();

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

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


Диагностика повторных регистраций

Полезно логировать момент подключения обработчика:

console.log('Подключение обработчика');

map.on('click', handler);

Если сообщение появляется многократно, значит регистрация происходит повторно.


Использование once()

Если событие должно быть обработано только один раз:

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

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


Обработчик невозможно удалить

Частая ошибка связана с использованием анонимных функций.

Неправильно:

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

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

Это разные объекты функций.

Удаление не произойдёт.

Правильно:

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

map.on('click', clickHandler);

map.off('click', clickHandler);

Теперь ссылка на функцию одинакова.


Утечки памяти из-за событий

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

Пример проблемы:

function createMap() {
    const map = new maplibregl.Map(...);

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

    return map;
}

Если карта удаляется:

map.remove();

Но внешние ссылки на обработчики сохраняются, память может освобождаться не полностью.

Особенно это заметно в SPA-приложениях.

Рекомендуется явно удалять обработчики:

map.off('move', moveHandler);

Перед уничтожением карты.


Проблемы с событием load

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

map.on('load', () => {
    console.log('Готово');
});

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

Однако разработчики часто ошибочно предполагают, что после него доступны абсолютно все данные.


Источники данных могут ещё загружаться

Пример:

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

Наличие события load не означает завершение загрузки всех тайлов и всех сетевых запросов.

Поэтому код вида:

map.on('load', () => {
    const features = map.queryRenderedFeatures();
});

Может возвращать неполные результаты.


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

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

map.on('idle', () => {
    console.log('Все операции завершены');
});

Оно вызывается тогда, когда:

  • отсутствуют активные анимации;
  • завершены запросы ресурсов;
  • карта ничего не перерисовывает.

Для анализа данных это часто более надёжный вариант.


События слоя не работают

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

Пример:

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

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


Слой отсутствует

Ошибка:

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

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

map.addLayer({
    id: 'cities',
    ...
});

На момент регистрации слой ещё не существует.

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

map.on('load', () => {
    map.addLayer(...);

    map.on('click', 'cities', handler);
});

Ошибка в идентификаторе слоя

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

Например:

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

При наличии слоя:

id: 'cities'

Обработчик никогда не сработает.


События вызываются слишком часто

Некоторые события генерируются десятки раз в секунду.

Примеры:

move
zoom
drag
render
mousemove

Проблемы производительности

Неправильно:

map.on('mousemove', () => {
    expensiveOperation();
});

Если курсор активно перемещается, функция может выполняться сотни раз в секунду.

Это приводит к:

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

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

Пример ограничения частоты вызовов:

function debounce(fn, delay) {
    let timeout;

    return (...args) => {
        clearTimeout(timeout);

        timeout = setTimeout(() => {
            fn(...args);
        }, delay);
    };
}

map.on('move', debounce(() => {
    console.log('Карта остановилась');
}, 300));

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

Для регулярного контроля частоты:

function throttle(fn, limit) {
    let waiting = false;

    return (...args) => {
        if (!waiting) {
            fn(...args);

            waiting = true;

            setTimeout(() => {
                waiting = false;
            }, limit);
        }
    };
}

Применение:

map.on('mousemove', throttle(handler, 100));

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

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

На практике порядок может зависеть от:

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

Отладка последовательности

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

[
    'load',
    'styledata',
    'sourcedata',
    'render',
    'idle'
].forEach(eventName => {
    map.on(eventName, () => {
        console.log(eventName);
    });
});

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


Ошибки при смене стиля

При вызове:

map.setStyle('new-style.json');

Многие разработчики ожидают сохранения обработчиков слоёв.

Однако новый стиль может полностью заменить набор слоёв.

Пример:

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

После смены стиля слой:

cities

Может исчезнуть.

Соответственно обработчик перестанет работать.


Повторная инициализация после style.load

Используется событие:

map.on('style.load', () => {
    initializeLayers();
});

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


События источников данных

Для диагностики загрузки данных используются:

sourcedata
dataloading
data

Пример:

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

Эти события помогают определить:

  • завершилась ли загрузка GeoJSON;
  • получены ли векторные тайлы;
  • произошла ли ошибка обновления источника.

Отладка объекта события

Практически любое событие содержит полезную информацию.

Например:

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

Для события клика доступны:

event.lngLat
event.point
event.originalEvent

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

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

console.dir(event);

Это позволяет увидеть фактическую структуру события.


Ошибки контекста this

При использовании классов часто возникает проблема потери контекста.

Пример:

class MapController {
    constructor(map) {
        this.map = map;

        map.on('click', this.handleClick);
    }

    handleClick() {
        console.log(this);
    }
}

Внутри метода значение this может оказаться неожиданным.


Использование bind()

class MapController {
    constructor(map) {
        this.map = map;

        this.handleClick =
            this.handleClick.bind(this);

        map.on('click', this.handleClick);
    }

    handleClick() {
        console.log(this.map);
    }
}

Использование стрелочных функций

class MapController {
    handleClick = () => {
        console.log(this.map);
    };
}

Стрелочная функция сохраняет внешний контекст.


Отладка через события ошибок

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

map.on('error', event => {
    console.error(event.error);
});

Через это событие можно выявить:

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

Игнорирование события error существенно усложняет диагностику.


Стратегии поиска проблем

Эффективная схема диагностики включает несколько последовательных шагов:

  1. Проверить регистрацию обработчика.
  2. Убедиться в корректности имени события.
  3. Проверить существование слоя или источника.
  4. Вывести объект события в консоль.
  5. Отследить порядок выполнения событий.
  6. Проверить повторные подписки.
  7. Проверить смену стилей.
  8. Подписаться на событие error.
  9. Использовать idle вместо преждевременной обработки данных.
  10. Контролировать высокочастотные события через debounce или throttle.

Систематическое применение этих приёмов позволяет быстро локализовать большинство проблем, связанных с событиями в MapLibre GL JS, даже в крупных интерактивных картографических приложениях.