memory-leaks
本文回答:内存泄漏排查 —— 未 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 排查
- Chrome DevTools → Memory → Heap Snapshot(打 Snapshot 1)
- 操作一下(比如打开关闭地图 10 次)
- 强制 GC(按垃圾桶图标)
- 打 Snapshot 2
- 对比 → 搜
Map/MapCadLayer/Marker数量 - 找 "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;观察趋势是否持续上涨。
十四、常见坑
- 组件多次 mount → 每次
map.on都加 → 第 N 次收到 N 个 handler。 eventBus.clear()太粗:清所有,包括 vjmapext 内部需要的。仅在mapcadLayer完全销毁时用。map.remove不清 Marker:手动。- Vue reactive 副作用 :响应式代理保留对组件的强引用;map 放
shallowRef。 - HMR 时地图变双:Vite HMR 后旧 map 没清;
import.meta.hot.dispose里销毁。
相关
- 事件清理 → event/cleanup
- 卸载 → setup/dispose
- Vue/React → trouble/vue-react-dispose