В архитектуре Mapbox GL JS данные рассматриваются как отдельный слой состояния, независимый от визуализации. Карта не хранит бизнес-логику приложения, а лишь отображает источники данных, которые могут изменяться динамически.
Базовые сущности работы с данными:
Синхронизация данных в этой модели означает поддержание актуальности
Source и корректное инициирование перерисовки слоёв без
полной реконструкции карты.
Наиболее гибкий тип источника, предназначенный для динамических данных:
map.addSource('points', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: []
}
});
Обновление данных происходит через setData:
map.getSource('points').setData(newGeojson);
Особенность модели заключается в том, что каждый вызов
setData инициирует:
Векторные тайлы обновляются иначе:
map.addSource('tiles', {
type: 'vector',
tiles: ['https://example.com/{z}/{x}/{y}.pbf']
});
Синхронизация данных здесь не локальная, а серверная: изменения проявляются при смене тайлов или сбросе кеша.
Для растровых данных обновление происходит через замену URL:
map.getSource('overlay').updateImage({
url: 'https://example.com/image.png'
});
Такие источники оптимизированы под редкие, но крупные обновления.
Mapbox реализует обновление карты через внутренний цикл рендеринга, основанный на запросах перерисовки.
Каждое изменение источника вызывает:
render frameВажно, что обновление не происходит синхронно в момент вызова
setData, а откладывается до следующего кадра.
При частых изменениях данных критично избегать многократных
setData:
function updatePoints(batch) {
const geojson = {
type: 'FeatureCollection',
features: batch
};
map.getSource('points').setData(geojson);
}
Практика: агрегирование изменений перед передачей в Mapbox.
Для синхронизации с рендер-циклом:
let pending = null;
function scheduleUpdate(features) {
pending = features;
requestAnimationFrame(() => {
if (!pending) return;
map.getSource('points').setData({
type: 'FeatureCollection',
features: pending
});
pending = null;
});
}
Типичный сценарий — потоковое обновление объектов:
const socket = new WebSocket('wss://example.com/stream');
socket.onmess age = (event) => {
const data = JSON.parse(event.data);
map.getSource('points').setData(data);
};
Основная проблема — частота сообщений. Без буферизации возможна деградация производительности из-за постоянного пересчёта геометрии.
Вместо полной замены данных используются диффы:
function applyDelta(source, delta) {
const current = source._data; // концептуально
const merged = applyChanges(current, delta);
source.setData(merged);
}
Хотя API Mapbox GL JS не предоставляет встроенного diff-режима для GeoJSON, паттерн часто реализуется на уровне приложения.
В сложных приложениях слой данных Mapbox синхронизируется с внешним state manager (Redux, Zustand и др.).
Типовая архитектура:
store.subscribe(() => {
const state = store.getState();
map.getSource('points').setData(state.geojson);
});
Ключевая проблема — предотвращение лишних перерисовок. Решается мемоизацией:
let lastHash = null;
store.subscribe(() => {
const state = store.getState();
const hash = hashGeojson(state.geojson);
if (hash === lastHash) return;
lastHash = hash;
map.getSource('points').setData(state.geojson);
});
Кластеры GeoJSON чувствительны к обновлениям источника:
map.addSource('clusters', {
type: 'geojson',
data: geojson,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Каждый setData приводит к:
При высоких объёмах данных рекомендуется:
Состояние источников может быть временно неконсистентным:
Для предотвращения «разрывов» используется стратегия:
map.once('idle', () => {
map.getSource('points').setData(nextData);
});
Или последовательная очередь обновлений:
const queue = [];
let updating = false;
function enqueue(data) {
queue.push(data);
process();
}
function process() {
if (updating || queue.length === 0) return;
updating = true;
const data = queue.shift();
map.getSource('points').setData(data);
map.once('idle', () => {
updating = false;
process();
});
}
GeoJSONSource не поддерживает частичное обновление feature. Любое изменение требует полной пересборки коллекции.
Следствие:
Альтернативные подходы:
Иногда требуется подстраивать данные под viewport:
map.on('moveend', () => {
const bounds = map.getBounds();
fetch(`/data?bbox=${bounds.toArray().flat().join(',')}`)
.then(res => res.json())
.then(data => {
map.getSource('points').setData(data);
});
});
Особенность: данные синхронизируются с состоянием камеры, что создаёт динамическую модель «viewport-driven data».
Внутренний механизм обновления карты строится вокруг концепции invalidation:
Система стремится минимизировать full re-render, ограничиваясь частичной перерисовкой WebGL сцен.
При одновременных обновлениях:
setData подрядвозникает проблема гонок состояния.
Решение:
let version = 0;
function update(data) {
const current = ++version;
map.getSource('points').setData(data);
requestAnimationFrame(() => {
if (current !== version) return;
});
}
Каждое обновление создаёт новый объект:
const next = {
...prev,
features: [...prev.features, newFeature]
};
Разделение данных по логическим группам:
const debounced = debounce((data) => {
map.getSource('points').setData(data);
}, 100);
Данные могут влиять на стиль отображения:
map.setPaintProperty('points-layer', 'circle-color', [
'case',
['>', ['get', 'value'], 10],
'red',
'blue'
]);
Изменение данных автоматически пересчитывает визуальные выражения, что добавляет дополнительный слой синхронизации между data-driven style expressions и source updates.
При высоконагруженных сценариях используется pipeline:
Эта модель позволяет удерживать стабильный FPS при непрерывных потоках обновлений геоданных.