Удаление обработчиков событий

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

В MapLibre GL JS события привязываются через метод map.on, который поддерживает несколько форм:

  • глобальные события карты (load, move, zoom, click)
  • события конкретных слоёв (click по слою, mouseenter, mouseleave)
  • события источников и тайловых данных (sourcedata, data)

Пример регистрации обработчика:

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

или с явной функцией:

function handleClick(e) {
    console.log(e.lngLat);
}

map.on('click', handleClick);

Именно второй вариант критичен для дальнейшего удаления, поскольку анонимная функция теряет идентифицируемую ссылку.

Принцип удаления обработчиков

Удаление выполняется через map.off, который повторяет сигнатуру map.on. Основное правило — структура вызова должна совпадать с регистрацией.

map.off('click', handleClick);

При этом важна строгая идентичность ссылки на функцию: новая функция с тем же телом не считается совпадением.

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

Глобальные события карты удаляются указанием только типа события и функции:

map.on('move', handleMove);

// удаление
map.off('move', handleMove);

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

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

// такой обработчик не может быть удалён отдельно

Обработчики событий слоёв

MapLibre GL JS поддерживает привязку событий к конкретным слоям:

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

Удаление требует полного совпадения всех параметров:

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

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

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

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

Частичное и массовое удаление обработчиков

Система допускает несколько уровней очистки:

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

map.off('mouseenter', 'poi-layer', handleHover);

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

map.off('click');

Удаляются все обработчики клика, включая глобальные и слой-ориентированные.

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

map.off('click', 'poi-layer');

Приводит к удалению всех кликов по слою независимо от функции.

Проблема анонимных функций

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

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

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

Корректная практика заключается в сохранении ссылки:

const onCl ick = (e) => {
    console.log(e);
};

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

Замыкания и потеря управляемости ссылкой

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

function createHandler(prefix) {
    return function (e) {
        console.log(prefix, e.lngLat);
    };
}

map.on('click', createHandler('A'));

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

const handler = createHandler('A');
map.on('click', handler);
map.off('click', handler);

Контекст и привязанные функции

Использование bind создаёт новую функцию-обёртку, что также влияет на удаление:

function handle(e) {
    console.log(this.name, e.lngLat);
}

const bound = handle.bind({ name: 'map' });

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

Попытка удалить исходную функцию не сработает:

map.off('click', handle); // не совпадает с bound

Очистка обработчиков при уничтожении карты

При вызове map.remove() большинство внутренних обработчиков освобождается автоматически, однако внешние ссылки на функции остаются в пользовательском коде. Это критично в сценариях повторного создания карты в одном контейнере.

Типичный сценарий:

map.off('click', handleClick);
map.off('move', handleMove);
map.remove();

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

Управление жизненным циклом в компонентной архитектуре

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

Логика обычно строится вокруг хранения ссылок:

let clickHandler;

function initMap(map) {
    clickHandler = (e) => {
        console.log(e.lngLat);
    };

    map.on('click', clickHandler);
}

function destroyMap(map) {
    map.off('click', clickHandler);
}

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

Повторная регистрация и дублирование обработчиков

При повторных вызовах map.on без удаления старых обработчиков происходит накопление вызовов:

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

В результате один клик вызывает функцию несколько раз. Удаление требует либо предварительного off, либо строгого контроля регистрации:

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

Особенности отсутствия неймспейсов

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

click.namespace

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

Централизованное управление подписками

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

const handlers = {
    click: [],
    move: []
};

function addHandler(map, type, fn) {
    handlers[type].push(fn);
    map.on(type, fn);
}

function removeAll(map, type) {
    handlers[type].forEach(fn => map.off(type, fn));
    handlers[type] = [];
}

Такой подход компенсирует отсутствие встроенной группировки событий.

Ошибки удаления обработчиков

Наиболее частые проблемы:

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

Каждая из этих ситуаций приводит к тому, что map.off не находит совпадение и обработчик остаётся активным.

Поведение при удалении частичных подписок

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

map.off('click', layerId);

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

Влияние порядка регистрации и удаления

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

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

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

В сложных системах поверх MapLibre GL JS часто вводятся обёртки, инкапсулирующие регистрацию и удаление:

class EventManager {
    constructor(map) {
        this.map = map;
        this.registry = new Map();
    }

    on(type, fn) {
        this.registry.set(fn, type);
        this.map.on(type, fn);
    }

    off(fn) {
        const type = this.registry.get(fn);
        if (type) {
            this.map.off(type, fn);
            this.registry.delete(fn);
        }
    }
}

Такой слой упрощает контроль жизненного цикла обработчиков и снижает вероятность утечек.

Поведение при повторной инициализации карты

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

Итоговая модель управления удалением

Удаление обработчиков в MapLibre GL JS опирается на три ключевых условия:

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

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