唯杰地图 logo唯杰地图
VJMAP
EN

vue-react-dispose

vjmap/references/trouble/vue-react-dispose.md 6.2 KB268 行
原文

本文回答: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。

十一、常见坑

  1. map 被 Vue reactive 代理:异常 / 性能差;必 shallowRef / markRaw。
  2. onBeforeUnmount 时 map 已被路由切走但 DOM 还没销毁:map.remove 顺序错;按本文顺序。
  3. keepAlive 下用 onBeforeUnmount:不触发;用 onDeactivated。
  4. 异步 init 卸载冲突:加 disposed 标志。
  5. React StrictMode 下 useEffect 跑两次:开发环境;生产不会。init/dispose 的幂等性要保证。

相关