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

Жизненный цикл карты в Leaflet определяется последовательностью создания, конфигурации, отображения, обновления и полного уничтожения экземпляра карты вместе со всеми связанными слоями, обработчиками событий и DOM-элементами. Корректное управление этим циклом критично для предотвращения утечек памяти, деградации производительности и неконсистентного поведения интерфейса, особенно в SPA-приложениях.


Инициализация карты и формирование базового состояния

Создание карты запускает формирование внутренней структуры объектов Leaflet: контейнера, системы координат, тайловой проекции и менеджера слоёв.

При вызове:

const map = L.map('map', {
  center: [51.505, -0.09],
  zoom: 13
});

происходит:

  • привязка к DOM-элементу
  • создание объекта состояния карты
  • инициализация системы координат (CRS)
  • подготовка контейнера слоёв (_panes)
  • создание обработчиков событий (_events)
  • установка начального view

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


Жизненный цикл DOM-контейнера

DOM-контейнер карты становится центральной точкой привязки всех визуальных элементов:

  • tile layers (тайлы)
  • markers (маркеры)
  • popups (всплывающие окна)
  • vector layers (GeoJSON, polylines, polygons)

При инициализации создаются внутренние слои-панели:

  • tilePane
  • overlayPane
  • markerPane
  • shadowPane
  • popupPane

Каждая панель управляет своим типом DOM-узлов. Удаление карты требует очистки всех этих узлов, иначе остаются «осиротевшие» элементы.


Добавление слоёв как этап жизненного цикла

Слой в Leaflet — это объект с собственным состоянием и методами onAdd и onRemove.

Этап добавления слоя

При layer.addTo(map) происходит:

  • регистрация слоя в map._layers
  • вызов onAdd(map)
  • создание DOM-элементов (если слой визуальный)
  • привязка событий к карте
  • добавление в соответствующую pane

Пример:

const marker = L.marker([51.5, -0.09]);
marker.addTo(map);

На этом этапе формируется полная связка: слой ↔︎ карта ↔︎ DOM ↔︎ события.


Удаление слоёв и разрыв связей

Удаление слоя через map.removeLayer(layer) или layer.remove() инициирует обратный процесс:

  • вызов onRemove(map)
  • удаление DOM-элементов
  • очистка событий
  • удаление из map._layers

Критически важно, что Leaflet не всегда автоматически освобождает пользовательские ссылки внутри кастомных слоёв. Если слой содержит внешние объекты (например, WebSocket, таймеры), они должны быть очищены вручную.


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

Система событий Leaflet построена на подписках через on, off, once.

Подписка:

map.on('moveend', handler);

Отписка:

map.off('moveend', handler);

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

Особое внимание требуется для:

  • move
  • zoom
  • resize
  • click
  • событий слоёв (layeradd, layerremove)

Тайловые слои и их жизненный цикл

TileLayer — один из самых ресурсоёмких компонентов.

При работе происходит:

  • вычисление видимых тайлов
  • генерация URL
  • загрузка изображений
  • кэширование DOM-элементов
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
  .addTo(map);

Этапы жизненного цикла:

  1. attach к карте
  2. вычисление bounds
  3. загрузка тайлов
  4. отрисовка в tilePane
  5. очистка невидимых тайлов при перемещении
  6. удаление при removeLayer

Без явного удаления слоя сетевые запросы могут продолжаться даже после исчезновения карты из DOM.


Маркеры и их внутренний цикл

Marker — объект с более сложной структурой:

  • DOM-элемент иконки
  • shadow-элемент
  • popup binding
  • drag handler (опционально)

При добавлении:

  • создаётся иконка
  • добавляется в markerPane
  • привязываются события мыши

При удалении:

  • DOM-элементы уничтожаются
  • события снимаются
  • drag-инстанс уничтожается

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


Popup и Tooltip имеют короткий жизненный цикл:

  • создаются по событию или вручную
  • привязываются к layer или map
  • отображаются поверх popupPane
  • уничтожаются при закрытии
marker.bindPopup('Text');
marker.openPopup();

Жизненный цикл включает:

  1. создание DOM-контейнера
  2. позиционирование
  3. привязка к координате
  4. удаление при close

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


GeoJSON слои и массовые структуры

GeoJSON layer агрегирует множество подслоёв:

L.geoJSON(data).addTo(map);

Внутренний процесс:

  • парсинг FeatureCollection
  • создание соответствующих Leaflet-слоёв
  • регистрация всех объектов в дереве слоёв

Жизненный цикл включает каскадное удаление:

  • GeoJSON layer → child layers → DOM elements → events

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


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

Обновления включают:

  • изменение центра
  • изменение zoom
  • изменение размеров контейнера

Ключевой метод:

map.invalidateSize();

Он перерасчитывает:

  • размеры viewport
  • положение тайлов
  • позиционирование слоёв

Отсутствие вызова после изменения DOM приводит к смещению тайлов и некорректной отрисовке.


Жизненный цикл в SPA-приложениях

При использовании React, Vue, Angular карта часто создаётся и уничтожается вместе с компонентом.

Основные этапы:

Монтирование

  • создание карты
  • добавление слоёв
  • регистрация событий

Обновление

  • изменение props → обновление layers
  • перерасчёт view

Размонтирование

Критический этап:

  • map.remove()
  • очистка всех слоёв
  • снятие событий
  • уничтожение DOM
map.off();
map.remove();
map = null;

Без этого остаются:

  • таймеры tile loading
  • listeners событий
  • ссылки на DOM

Утечки памяти и причины их возникновения

Основные источники:

1. Неснятые события

map.on('move', handler);

без off.

2. Кастомные слои

С внешними ресурсами:

  • WebSocket
  • setInterval
  • requestAnimationFrame

3. Незакрытые popups

С привязанными обработчиками.

4. DOM-ссылки вне Leaflet

Хранимые вручную элементы иконок.


Уничтожение карты и полная очистка

Полное удаление включает:

  • удаление DOM-контейнера
  • очистку внутренних слоёв
  • снятие событий
  • освобождение тайлов
  • удаление ссылок на карту
map.eachLayer(layer => {
  map.removeLayer(layer);
});

map.off();
map.remove();

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


Поведение внутренних панелей при удалении

Каждая pane очищается отдельно:

  • tilePane → изображения тайлов
  • markerPane → маркеры
  • popupPane → всплывающие окна

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


Особенности повторной инициализации

Повторное создание карты на том же контейнере требует:

  • полного удаления старого экземпляра
  • очистки innerHTML контейнера
  • сброса ссылок
map.remove();
document.getElementById('map').innerHTML = '';

Иначе возникает конфликт внутренних состояний Leaflet.


Состояние карты как конечная машина состояний

Жизненный цикл можно представить как конечный автомат:

  • INIT → CREATED
  • CREATED → ACTIVE
  • ACTIVE → UPDATED
  • ACTIVE → DESTROYED

Переход в DESTROYED должен быть финальным: любые обращения к карте после него приводят к неопределённому поведению.


Управление ресурсами тайлового движка

Tile loading использует:

  • XHR / fetch запросы
  • кэш браузера
  • очередь загрузки

При удалении карты:

  • запросы могут завершаться асинхронно
  • необходимо учитывать race conditions
  • DOM-элементы могут быть удалены до завершения загрузки

Итоговая структура жизненного цикла

  • создание контейнера
  • инициализация карты
  • добавление слоёв
  • активное взаимодействие
  • обновления состояния
  • удаление слоёв
  • снятие событий
  • уничтожение карты
  • очистка DOM и ресурсов