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

Жизненный цикл в Mapbox GL JS представляет собой последовательность состояний и событий, через которые проходит объект карты с момента создания до полного уничтожения. Грамотное управление жизненным циклом позволяет:

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

Основным объектом жизненного цикла выступает экземпляр класса mapboxgl.Map.

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',
    center: [37.6176, 55.7558],
    zoom: 10
});

После выполнения конструктора начинается процесс инициализации внутренних компонентов карты.


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

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

  1. Создание экземпляра.
  2. Инициализация WebGL-контекста.
  3. Загрузка стиля.
  4. Загрузка источников данных.
  5. Загрузка тайлов.
  6. Отрисовка карты.
  7. Работа пользователя.
  8. Изменение состояния карты.
  9. Уничтожение объекта.

Схематически процесс выглядит следующим образом:

Создание Map
      ↓
Загрузка Style
      ↓
Загрузка Sources
      ↓
Загрузка Tiles
      ↓
Первый Render
      ↓
Интерактивная работа
      ↓
Изменение данных
      ↓
Удаление карты

Создание экземпляра карты

Инициализация начинается после вызова конструктора.

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/light-v11'
});

В этот момент происходит:

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

Карта еще не готова для полноценной работы.

Например, следующий код может завершиться ошибкой:

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/light-v11'
});

map.addLayer({
    id: 'test',
    type: 'circle',
    source: 'points'
});

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


Событие load

Наиболее распространенная точка входа в жизненный цикл — событие load.

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

Событие возникает после:

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

После получения события становится безопасно:

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

Пример:

map.on('load', () => {
    map.addSource('cities', {
        type: 'geojson',
        data: '/data/cities.geojson'
    });

    map.addLayer({
        id: 'cities-layer',
        type: 'circle',
        source: 'cities'
    });
});

Событие style.load

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

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

Это особенно важно при динамической смене стилей.

map.setStyle(
    'mapbox://styles/mapbox/satellite-streets-v12'
);

После выполнения setStyle() многие пользовательские настройки удаляются:

  • слои;
  • источники данных;
  • изображения;
  • некоторые настройки визуализации.

Поэтому часто используется повторное создание ресурсов:

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

Проверка готовности карты

Для контроля текущего состояния существуют специальные методы.

isStyleLoaded

Проверяет загрузку стиля.

if (map.isStyleLoaded()) {
    console.log('Стиль готов');
}

isSourceLoaded

Проверяет готовность конкретного источника.

if (map.isSourceLoaded('cities')) {
    console.log('Источник загружен');
}

loaded

Проверяет готовность карты к взаимодействию.

if (map.loaded()) {
    console.log('Карта полностью готова');
}

Метод полезен при сложной асинхронной логике.


События загрузки данных

Mapbox GL JS предоставляет набор событий для отслеживания загрузки ресурсов.

sourcedata

Вызывается при изменении состояния источников.

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

Применяется для:

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

dataloading

Срабатывает при начале загрузки данных.

map.on('dataloading', () => {
    console.log('Загрузка данных');
});

data

Возникает после получения данных.

map.on('data', () => {
    console.log('Данные получены');
});

styledata

Сообщает об изменениях стиля.

map.on('styledata', () => {
    console.log('Стиль обновлен');
});

Полезно при динамическом конфигурировании оформления.


Событие idle

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

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

Событие возникает, когда:

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

Это удобная точка для:

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

Пример:

map.on('idle', () => {
    exportMapImage();
});

Цикл рендеринга

Mapbox GL JS использует непрерывный цикл отрисовки.

Каждый кадр включает:

  1. Проверку изменений.
  2. Пересчет трансформаций.
  3. Обновление источников.
  4. Подготовку буферов WebGL.
  5. Отрисовку слоев.

Событие render

Срабатывает при каждом кадре.

map.on('render', () => {
    console.log('Кадр отрисован');
});

Использовать его следует осторожно.

Нежелательный пример:

map.on('render', () => {
    expensiveCalculation();
});

Подобный код способен значительно снизить FPS.


Событие renderstart

Вызывается перед началом очередной отрисовки.

map.on('renderstart', () => {
    console.log('Начало рендера');
});

Применяется редко, преимущественно для диагностики.


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

Пользовательские слои также имеют собственный жизненный цикл.

При использовании Custom Layer Interface доступны методы:

const customLayer = {
    id: 'custom',
    type: 'custom',

    onAdd(map, gl) {
        console.log('Слой создан');
    },

    render(gl, matrix) {
        console.log('Отрисовка');
    }
};

onAdd

Вызывается один раз после подключения слоя.

onAdd(map, gl) {
    this.program = createProgram(gl);
}

Обычно здесь создаются:

  • шейдеры;
  • текстуры;
  • буферы;
  • WebGL-программы.

render

Вызывается на каждом кадре.

render(gl, matrix) {
    gl.drawArrays(
        gl.TRIANGLES,
        0,
        6
    );
}

Здесь располагается код отрисовки.


Освобождение ресурсов

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

Например:

gl.deleteBuffer(buffer);
gl.deleteTexture(texture);
gl.deleteProgram(program);

Иначе возможно накопление ресурсов GPU.


Изменение жизненного цикла через setStyle

Смена стиля фактически запускает новую фазу жизненного цикла.

map.setStyle(
    'mapbox://styles/mapbox/dark-v11'
);

В процессе:

  1. Старый стиль выгружается.
  2. Удаляются пользовательские слои.
  3. Загружается новый стиль.
  4. Создаются новые внутренние структуры.
  5. Запускается повторная отрисовка.

Поэтому рекомендуется повторно регистрировать данные:

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

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

Источник проходит несколько этапов:

Создание
    ↓
Регистрация
    ↓
Загрузка данных
    ↓
Обработка
    ↓
Использование слоями
    ↓
Обновление
    ↓
Удаление

Создание:

map.addSource('roads', {
    type: 'geojson',
    data: roadsData
});

Обновление:

map.getSource('roads')
   .setData(newData);

Удаление:

map.removeSource('roads');

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

map.removeLayer('roads-layer');
map.removeSource('roads');

Жизненный цикл изображений

Изображения используются в символических слоях.

Добавление:

map.loadImage('/marker.png', (error, image) => {
    if (error) throw error;

    map.addImage('marker', image);
});

Проверка:

map.hasImage('marker');

Удаление:

map.removeImage('marker');

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


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

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

Создание:

function handleMove() {
    console.log('move');
}

map.on('move', handleMove);

Удаление:

map.off('move', handleMove);

Особенно важно для:

  • React;
  • Vue;
  • Angular;
  • SPA-приложений.

Накопление обработчиков может привести к многократному выполнению одного и того же кода.


Уничтожение карты

Завершающий этап жизненного цикла — уничтожение экземпляра карты.

Для этого используется метод:

map.remove();

После вызова происходит:

  • удаление DOM-элементов;
  • остановка рендеринга;
  • освобождение WebGL-ресурсов;
  • удаление внутренних слушателей;
  • завершение сетевых операций.

После уничтожения объект становится недействительным.

Неверный вариант:

map.remove();

map.flyTo({
    center: [0, 0]
});

Подобные вызовы приводят к ошибкам.


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

Наиболее распространенный сценарий — создание карты при монтировании компонента и удаление при размонтировании.

useEffect(() => {
    const map = new mapboxgl.Map({
        container: mapContainer.current,
        style: 'mapbox://styles/mapbox/streets-v12'
    });

    return () => {
        map.remove();
    };
}, []);

Такой подход гарантирует:

  • отсутствие утечек памяти;
  • освобождение GPU-ресурсов;
  • корректное завершение жизненного цикла.

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

Пример для Vue:

import { onMounted, onUnmounted } from 'vue';

let map;

onMounted(() => {
    map = new mapboxgl.Map({
        container: 'map',
        style: 'mapbox://styles/mapbox/streets-v12'
    });
});

onUnmounted(() => {
    map.remove();
});

Карта существует только в период жизни компонента.


Типичные ошибки управления жизненным циклом

Добавление слоев до загрузки карты

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

const map = new mapboxgl.Map(config);

map.addLayer(layer);

Правильно:

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

Отсутствие удаления карты

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

componentWillUnmount() {
}

Правильно:

componentWillUnmount() {
    map.remove();
}

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

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

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

Правильно:

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

Удаление источника до удаления слоя

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

map.removeSource('roads');

Правильно:

map.removeLayer('roads-layer');
map.removeSource('roads');

Практическая схема управления жизненным циклом

Для крупных приложений эффективной считается следующая последовательность:

Создание карты
      ↓
load
      ↓
Регистрация источников
      ↓
Регистрация слоев
      ↓
Подписка на события
      ↓
Работа пользователя
      ↓
Обновление данных
      ↓
Удаление событий
      ↓
Удаление слоев
      ↓
Удаление источников
      ↓
remove()

Такой подход обеспечивает предсказуемое поведение карты, стабильное использование памяти, корректную работу WebGL и надежное управление всеми объектами Mapbox GL JS на протяжении полного жизненного цикла приложения.