> **本文回答**：内存泄漏排查 —— 未 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 销毁的正确顺序

```js
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 泄漏

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

检测：Chrome DevTools → Elements 搜 `vjmapgis-marker`，数量对不上。

## 四、事件监听泄漏

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

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

## 五、定时器泄漏

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

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

## 六、RAF 泄漏

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

// 卸载
cancelAnimationFrame(rafId);
```

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

## 七、Worker 泄漏

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

// 卸载
w.terminate();
```

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

## 八、Vue / React 卸载模板

### Vue 3

```js
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

```jsx
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()` 还被业务持有

## 十一、闭包泄漏

```js
// 错：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 级别的全局清理；**必须手动销毁**。典型：

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

## 十三、监控工具

```js
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` 里销毁。

## 相关

- 事件清理 → [event/cleanup](../event/cleanup.md)
- 卸载 → [setup/dispose](../setup/dispose.md)
- Vue/React → [trouble/vue-react-dispose](../trouble/vue-react-dispose.md)
