Методы onAdd и onRemove

В архитектуре Mapbox GL JS пользовательские элементы управления (controls) интегрируются в карту через единый контракт жизненного цикла. Этот контракт определяет два ключевых метода: onAdd(map) и onRemove(map). Они обеспечивают предсказуемую и управляемую интеграцию DOM-элементов, событий и логики управления состоянием.

Контрол в Mapbox GL JS представляет собой объект, который реализует интерфейс:

  • onAdd(map) — создание и добавление элемента в DOM карты
  • onRemove(map) — удаление элемента и очистка ресурсов

Дополнительно часто присутствует метод getDefaultPosition, но именно пара onAdd/onRemove формирует основу жизненного цикла.


Метод onAdd: инициализация и подключение к карте

Метод onAdd(map) вызывается автоматически при добавлении контрола в карту через map.addControl(control).

Основная задача метода — создать DOM-узел, связать его с экземпляром карты и подготовить внутреннюю логику.

Сигнатура:

onAdd(map: Map): HTMLElement

Основные обязанности onAdd

При вызове метода обычно выполняются следующие действия:

  1. Создание корневого DOM-элемента контрола
  2. Сохранение ссылки на экземпляр карты
  3. Регистрация обработчиков событий
  4. Инициализация внутреннего состояния
  5. Возврат DOM-элемента для вставки в интерфейс карты

Базовый пример реализации

class CustomControl {
    onAdd(map) {
        this.map = map;

        this.container = document.createElement('div');
        this.container.className = 'mapboxgl-ctrl custom-control';

        this.button = document.createElement('button');
        this.button.textContent = 'Reset';

        this.container.appendChild(this.button);

        this.onCl ick = this.onClick.bind(this);
        this.button.addEventListener('click', this.onClick);

        return this.container;
    }

    onClick() {
        this.map.resetNorthPitch();
    }
}

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

Создание DOM

Контрол обязан создавать собственный контейнер. Mapbox GL JS не предоставляет готового DOM-узла, поэтому разработчик полностью контролирует структуру.

Сохранение ссылки на карту

this.map = map;

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

Подписка на события

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

this.map.on('move', this.onMapMove);

Именование и изоляция

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


Метод onRemove: корректное уничтожение и очистка ресурсов

Метод onRemove(map) вызывается при удалении контрола через map.removeControl(control).

Сигнатура:

onRemove(map: Map): void

Основные задачи onRemove

  1. Удаление DOM-элементов
  2. Снятие обработчиков событий
  3. Очистка ссылок на карту
  4. Освобождение памяти
  5. Прерывание асинхронных процессов (если есть)

Базовая реализация

class CustomControl {
    onAdd(map) {
        this.map = map;

        this.container = document.createElement('div');
        this.container.className = 'mapboxgl-ctrl custom-control';

        this.onM ove = this.onMove.bind(this);
        this.map.on('move', this.onMove);

        return this.container;
    }

    onRemove() {
        this.map.off('move', this.onMove);
        this.container.remove();

        this.map = undefined;
    }

    onMove() {
        console.log('map moved');
    }
}

Утечки памяти и важность корректного onRemove

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

  • утечки DOM-узлов
  • накопление event listeners
  • сохранение ссылок на карту
  • невозможность garbage collection

Типичный пример ошибки

onRemove() {
    // ошибка: обработчик не удалён
    this.container.remove();
}

В этом случае подписка на события остаётся активной:

this.map.on('move', this.handler);

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


Взаимодействие onAdd и onRemove в архитектуре контролов

onAdd и onRemove образуют симметричную пару, которая определяет жизненный цикл компонента:

  • onAdd — установка всех связей
  • onRemove — полное их разрушение

Контролы в Mapbox GL JS проектируются как изолированные модули, где вся побочная активность должна быть привязана к этим двум точкам.

Структурный шаблон контролов

class StructuredControl {
    onAdd(map) {
        this.map = map;

        this._createUI();
        this._bindEvents();

        return this.container;
    }

    onRemove() {
        this._unbindEvents();
        this._destroyUI();

        this.map = undefined;
    }
}

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


Работа с асинхронными процессами

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

Таймеры

onAdd(map) {
    this.map = map;

    this.interval = setInterval(() => {
        console.log('tick');
    }, 1000);

    return document.createElement('div');
}

onRemove() {
    clearInterval(this.interval);
}

Запросы и отмена

onAdd(map) {
    this.controller = new AbortController();

    fetch('/data', { signal: this.controller.signal })
        .then(r => r.json())
        .then(data => this.handleData(data));

    return document.createElement('div');
}

onRemove() {
    this.controller.abort();
}

Контекст исполнения и привязка this

В onAdd часто возникает необходимость привязки методов к экземпляру контролла.

this.handler = this.handler.bind(this);

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

this.handler = () => {
    console.log(this.map);
};

Корректная привязка важна, поскольку Mapbox GL JS вызывает методы контрола в собственном контексте только для onAdd и onRemove, остальные методы выполняются в контексте объекта.


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

Контролы часто взаимодействуют с состоянием карты:

  • добавление/удаление слоёв
  • изменение стилей
  • управление источниками данных
onAdd(map) {
    this.map = map;

    this.button = document.createElement('button');
    this.button.oncl ick = () => {
        const visibility = this.map.getLayoutProperty('roads', 'visibility');
        this.map.setLayoutProperty(
            'roads',
            'visibility',
            visibility === 'visible' ? 'none' : 'visible'
        );
    };

    return this.button;
}

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


Повторное использование контролов

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

Корректная реализация:

  • не предполагает одноразового использования
  • не хранит глобального состояния вне экземпляра
  • полностью восстанавливается через onAdd
map.addControl(control);
map.removeControl(control);
map.addControl(control);

Повторное добавление должно работать без побочных эффектов.


Разделение ответственности между методами

Этап onAdd onRemove
DOM создание удаление
События подписка отписка
Состояние инициализация очистка
Карта сохранение ссылки освобождение ссылки
Асинхронность запуск остановка

Чёткое соблюдение этого разделения является базовым требованием корректной интеграции с Mapbox GL JS.