vue-react-dispose
本文回答:Vue / React 组件卸载时地图没释放 —— 正确的销毁顺序 + 响应式陷阱。
一、症状
- 组件切走后 canvas 继续在 DOM 里(或不在 DOM 但内存不降)
- 多次进出页面后页面越来越慢
- 热更新(HMR)后地图变"重影"
- keepAlive 下 Marker 积累
二、销毁顺序(不可颠倒)
1. 取消业务事件监听 (off)
2. 清 Marker / Popup
3. 清 mapcadLayer eventBus
4. map.removeControl(mapcadLayer)
5. 清定时器 / RAF / Worker
6. map.remove()
7. 清 DOM 容器(通常 Vue/React 自动)
三、Vue 3 Options / setup 模板
<template>
<div ref="mapEl" class="map"></div>
</template>
<script setup>
import { ref, shallowRef, onMounted, onBeforeUnmount } from 'vue';
const mapEl = ref(null);
const map = shallowRef(null); // ★ shallowRef
const mapcadLayer = shallowRef(null);
const markers = [];
const handlers = [];
function on(target, evt, h) {
target.on(evt, h);
handlers.push(() => target.off(evt, h));
}
onMounted(async () => {
map.value = new vjmap.Map({ container: mapEl.value });
await map.value.onLoad();
mapcadLayer.value = new vjmapext.MapCadLayer();
map.value.addControl(mapcadLayer.value); // onAdd 同步完成,无 ready 钩子
const m = new vjmap.Marker().setLngLat([0, 0]);
m.addTo(map.value);
markers.push(m);
on(map.value, 'click', e => console.log(e.lngLat));
on(mapcadLayer.value.eventBus, 'entity:added', ({ entity }) => console.log(entity.id));
});
onBeforeUnmount(() => {
handlers.forEach(fn => fn());
handlers.length = 0;
markers.forEach(m => m.remove());
markers.length = 0;
if (mapcadLayer.value) {
mapcadLayer.value.eventBus.clear();
map.value?.removeControl(mapcadLayer.value);
mapcadLayer.value = null;
}
map.value?.remove();
map.value = null;
});
</script>
四、Vue Options API
export default {
data() {
return { markers: [], handlers: [] };
},
mounted() {
this.map = markRaw(new vjmap.Map(...)); // markRaw 防响应式代理
// ...
},
beforeUnmount() {
this.handlers.forEach(fn => fn());
this.markers.forEach(m => m.remove());
this.map?.remove();
}
};
五、Vue keepAlive 场景
<keep-alive>
<router-view />
</keep-alive>
组件切走用 onDeactivated,不是 onBeforeUnmount:
import { onActivated, onDeactivated } from 'vue';
onDeactivated(() => {
// 组件被 cache,按需释放资源
markers.forEach(m => m.remove());
markers.length = 0;
});
onActivated(() => {
// 重新创建
});
或不 keepAlive 地图页。
六、React 完整模板
import { useEffect, useRef } from 'react';
export default function MapView() {
const mapEl = useRef(null);
const mapRef = useRef(null);
const mapcadLayerRef = useRef(null);
const markersRef = useRef([]);
useEffect(() => {
let disposed = false;
let cleanup = [];
(async () => {
const map = new vjmap.Map({ container: mapEl.current });
await map.onLoad();
if (disposed) { map.remove(); return; }
mapRef.current = map;
const mapcadLayer = new vjmapext.MapCadLayer();
map.addControl(mapcadLayer);
if (disposed) { map.removeControl(mapcadLayer); map.remove(); return; }
mapcadLayerRef.current = mapcadLayer;
const m = new vjmap.Marker().setLngLat([0, 0]);
m.addTo(map);
markersRef.current.push(m);
const onClick = e => console.log(e.lngLat);
map.on('click', onClick);
cleanup.push(() => map.off('click', onClick));
})();
return () => {
disposed = true;
cleanup.forEach(fn => fn());
markersRef.current.forEach(m => m.remove());
markersRef.current = [];
if (mapcadLayerRef.current) {
mapcadLayerRef.current.eventBus.clear();
mapRef.current?.removeControl(mapcadLayerRef.current);
mapcadLayerRef.current = null;
}
mapRef.current?.remove();
mapRef.current = null;
};
}, []);
return <div ref={mapEl} className="map" />;
}
注意异步 init:卸载时 map 可能还没 onLoad 完成,用 disposed 标志防竞态。
七、响应式陷阱
Vue reactive 代理 map 对象
const state = reactive({ map: null });
state.map = new vjmap.Map(...);
// ❌ map 被 Proxy 包裹;很多内部 this 指向不对
必用 shallowRef 或 markRaw:
const map = shallowRef(null);
// 或
state.map = markRaw(new vjmap.Map(...));
React useState 重渲染
const [map, setMap] = useState(null);
setMap(new vjmap.Map(...)) 会导致组件 re-render;通常不是问题,但 map 对象当依赖放 useEffect 的 [] 会反复运行。用 useRef:
const mapRef = useRef(null);
八、HMR 支持
if (import.meta.hot) {
import.meta.hot.dispose(() => {
markers.forEach(m => m.remove());
map?.remove();
});
}
九、异步初始化 + 快速卸载
let disposed = false;
onBeforeUnmount(() => { disposed = true; });
(async () => {
const map = new vjmap.Map(...);
await map.onLoad();
if (disposed) {
map.remove();
return;
}
// continue init
})();
避免 "用户进页面 → 立刻切走 → 异步 init 完成 → map 无法释放"。
十、SSR / Nuxt
SSR 下没 window / document:
// Nuxt 3
<script setup>
import { onMounted } from 'vue';
onMounted(async () => {
const vjmap = await import('vjmap');
const map = new vjmap.Map(...);
});
</script>
用动态 import 避免 SSR 阶段加载 vjmap。
十一、常见坑
- map 被 Vue reactive 代理:异常 / 性能差;必 shallowRef / markRaw。
- onBeforeUnmount 时 map 已被路由切走但 DOM 还没销毁:
map.remove顺序错;按本文顺序。 - keepAlive 下用 onBeforeUnmount:不触发;用 onDeactivated。
- 异步 init 卸载冲突:加
disposed标志。 - React StrictMode 下 useEffect 跑两次:开发环境;生产不会。init/dispose 的幂等性要保证。
相关
- Marker 重影 → marker-duplicate
- 监听泄漏 → listener-leak
- 卸载 → setup/dispose
- Vue/React 集成 → setup/integrations