唯杰地图 logo唯杰地图
VJMAP
EN

memory-leaks

vjmap/references/perf/memory-leaks.md 5.0 KB221 行
原文

本文回答:内存泄漏排查 —— 未 off 监听、未 remove Marker、未卸载 plugin、未 destroy worker。

一、最常见的 5 种泄漏

# 泄漏源 症状
1 map.on(...) 没 off 组件切换后回调仍跑;持有旧组件实例无法 GC
2 marker.addTo(map) 没 remove Marker DOM 留在页面
3 mapcadLayer.eventBus.on(...) 没 off 同 1
4 定时器 / RAF 没 clear 持续跑,持有整张 map
5 Worker 没 terminate 独立线程继续占内存

二、map 销毁的正确顺序

function disposeMap() {
  // 1. 取消所有事件监听(业务层自己的)
  myListeners.clear();

  // 2. 清 Marker(map.remove 不会清外部 Marker)
  for (const m of markers) m.remove();
  markers.length = 0;

  // 3. 清 mapcadLayer
  mapcadLayer.eventBus.clear();
  map.removeControl(mapcadLayer);

  // 4. 定时器 / RAF
  for (const id of timers) clearInterval(id);
  if (rafId) cancelAnimationFrame(rafId);

  // 5. Worker
  for (const w of workers) w.terminate();

  // 6. 最后销毁 map
  map.remove();

  // 7. 清 DOM
  container.innerHTML = '';
}

三、Marker 泄漏

// 错:组件卸载后 marker 仍在
let marker;
onMount(() => {
  marker = new vjmap.Marker().setLngLat([0, 0]);
  marker.addTo(map);   // addTo 返回 undefined,勿接链尾
});
// 忘了 onUnmount { marker.remove() }

检测:Chrome DevTools → Elements 搜 vjmapgis-marker,数量对不上。

四、事件监听泄漏

// 错
map.on('click', () => { ... });        // 匿名 → 永远 off 不掉

// 对
const h = () => { ... };
map.on('click', h);
onUnmount(() => map.off('click', h));

五、定时器泄漏

// 错
setInterval(() => updateMarkers(), 1000);     // 忘保存 id

// 对
const id = setInterval(...);
onUnmount(() => clearInterval(id));

六、RAF 泄漏

let rafId;
function tick() {
  // update entities
  rafId = requestAnimationFrame(tick);
}
tick();

// 卸载
cancelAnimationFrame(rafId);

不 cancel 的话动画继续跑,持续持有场景对象。

七、Worker 泄漏

const w = new Worker('...');
w.postMessage(data);
w.onmessage = e => handle(e.data);

// 卸载
w.terminate();

Worker 是独立线程,不 terminate 会一直存活。

八、Vue / React 卸载模板

Vue 3

import { onBeforeUnmount } from 'vue';

const listeners = [];
function on(target, evt, h) {
  target.on(evt, h);
  listeners.push(() => target.off(evt, h));
}

on(map, 'click', onClick);
on(map, 'moveend', onMoveEnd);

onBeforeUnmount(() => {
  listeners.forEach(fn => fn());
  mapcadLayer.eventBus.clear();
  markers.forEach(m => m.remove());
  clearInterval(timerId);
  map.remove();
});

React

useEffect(() => {
  const handlers = [];
  const on = (t, e, h) => {
    t.on(e, h);
    handlers.push(() => t.off(e, h));
  };
  on(map, 'click', onClick);

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

九、Heap Snapshot 排查

  1. Chrome DevTools → Memory → Heap Snapshot(打 Snapshot 1)
  2. 操作一下(比如打开关闭地图 10 次)
  3. 强制 GC(按垃圾桶图标)
  4. 打 Snapshot 2
  5. 对比 → 搜 Map / MapCadLayer / Marker 数量
  6. 找 "Retained by" 路径定位引用链

十、Detached DOM

Chrome DevTools → Memory → Detached elements:

  • 显示已从文档移除但仍被 JS 引用的 DOM
  • Marker 常出问题:remove 了但 marker.getElement() 还被业务持有

十一、闭包泄漏

// 错:handler 闭包捕获 hugeData
const hugeData = new Array(1_000_000);
function setup(map) {
  map.on('click', e => {
    if (e.lngLat.lng > 0) console.log(hugeData[0]);
  });
}
setup(map);
// hugeData 永远无法 GC(被 handler 捕获)

解决:handler 外部尽量少捕获大对象;必要时 const ref = hugeData[0] 只捕获小字段。

十二、单页应用路由切换

SPA 路由切换不会触发浏览器 GC 级别的全局清理;必须手动销毁。典型:

router.beforeEach((to, from) => {
  if (from.path === '/map' && to.path !== '/map') {
    disposeMap();
  }
});

十三、监控工具

setInterval(() => {
  if (performance.memory) {
    console.log(`JSHeap: ${(performance.memory.usedJSHeapSize / 1e6).toFixed(1)} MB`);
  }
}, 5000);

Chrome only;观察趋势是否持续上涨。

十四、常见坑

  1. 组件多次 mount → 每次 map.on 都加 → 第 N 次收到 N 个 handler。
  2. eventBus.clear() 太粗:清所有,包括 vjmapext 内部需要的。仅在 mapcadLayer 完全销毁时用。
  3. map.remove 不清 Marker:手动。
  4. Vue reactive 副作用 :响应式代理保留对组件的强引用;map 放 shallowRef。
  5. HMR 时地图变双:Vite HMR 后旧 map 没清;import.meta.hot.dispose 里销毁。

相关