> **本文回答**：监听器未 off 导致内存泄漏 —— 现象、定位、修法。

## 一、症状

- 长时间使用后内存持续上涨，不会回落
- 切换页面后点击依旧 fire 回调（你以为已经卸载了）
- 控制台长时间挂着后明显变慢

## 二、根因

`map.on(...)` 或 `eventBus.on(...)` 注册后，回调函数会被内部长期持有；回调的**闭包**又持有外层变量 / DOM / 组件实例 → **整条链**无法 GC。

## 三、典型泄漏模式

### 1 匿名函数不 off

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

### 2 重复 setup

```js
function setup() {
  map.on('click', onClick);
}
setup(); setup(); setup();   // 一次 click fire 3 次
```

### 3 忘 off

```js
const h = () => updatePanel();
map.on('moveend', h);
// 组件卸载时：没 off
```

## 四、修法

### A. 保留 handler 引用

```js
const onClick = e => { ... };
map.on('click', onClick);
// 卸载
map.off('click', onClick);
```

### B. 统一集合管理

```js
const subs = [];
function on(target, event, h) {
  target.on(event, h);
  subs.push(() => target.off(event, h));
}

on(map, 'click', onClick);
on(map, 'moveend', onMoveEnd);
on(mapcadLayer.eventBus, VjmapextEvents.EntityAdded, onEntityAdded);

// 卸载
subs.forEach(fn => fn());
subs.length = 0;
```

### C. eventBus.clear

```js
mapcadLayer.eventBus.clear();       // 清所有订阅（慎用）
```

**注意**：会清掉 vjmapext 内部自己的监听；仅在 `mapcadLayer` 被整体销毁时用。

### D. once

```js
map.once('load', () => { ... });
```

自动解绑，避免泄漏。

## 五、定位泄漏

### 方法 1：Chrome Memory

1. 打开页面，Memory → Take heap snapshot（快照 1）
2. 做操作（进出页面 5~10 次）
3. 点"垃圾桶"强制 GC
4. 再 Take heap snapshot（快照 2）
5. 选 Snapshot 2 → Comparison 快照 1
6. 搜 `Map` / `MapCadLayer` / `Marker` → 增量 > 0 说明泄漏

### 方法 2：showRetainers

1. 在 Snapshot 找到泄漏对象
2. 右下 "Retainers" 面板
3. 看这个对象被哪条引用链持有
4. 一般是 `Window → MyComponent → handler → Closure → map / Marker`
5. 定位到 handler → 代码里找对应 `on` 调用 → 补 off

## 六、Vue 3 响应式 map 变量

```js
const map = reactive(null);        // ❌ 不要 reactive wrap map
```

Vue 响应式代理会持有 map 的强引用；组件卸载后可能因为其它 watch 仍然引用 map 造成额外泄漏。

```js
const map = shallowRef(null);      // ✅ 浅引用
```

## 七、SetInterval / RAF 泄漏

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

必须保存 id 并在卸载时 clear：

```js
const id = setInterval(() => updateMarkers(), 1000);
// 卸载
clearInterval(id);
```

RAF 同理：

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

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

## 八、跨组件的 eventBus 泄漏

组件 A 订阅 eventBus 事件；组件 A 卸载但没 off → eventBus 仍持有 A 的 handler → A 的 render 函数 / 整个组件实例 无法回收：

```js
onBeforeUnmount(() => {
  mapcadLayer.eventBus.off(VjmapextEvents.SelectionChanged, onSelChanged);
});
```

## 九、DOM 事件泄漏

```js
container.addEventListener('contextmenu', onCtx);
// 卸载
container.removeEventListener('contextmenu', onCtx);
```

## 十、批量事件 off helper

```js
function Listenable() {
  this.subs = [];
}
Listenable.prototype.on = function(t, e, h) {
  t.on(e, h);
  this.subs.push(() => t.off(e, h));
};
Listenable.prototype.onDom = function(el, e, h) {
  el.addEventListener(e, h);
  this.subs.push(() => el.removeEventListener(e, h));
};
Listenable.prototype.timeout = function(fn, ms) {
  const id = setTimeout(fn, ms);
  this.subs.push(() => clearTimeout(id));
};
Listenable.prototype.interval = function(fn, ms) {
  const id = setInterval(fn, ms);
  this.subs.push(() => clearInterval(id));
};
Listenable.prototype.off = function() {
  this.subs.forEach(fn => fn());
  this.subs.length = 0;
};

const L = new Listenable();
L.on(map, 'click', onClick);
L.on(mapcadLayer.eventBus, 'entity:added', onAdded);
L.onDom(container, 'contextmenu', onCtx);
L.interval(refresh, 1000);

// 卸载
L.off();
```

## 十一、常见坑

1. **箭头函数 inline on**：无法 off；必须保存引用。
2. **Vue reactive map**：强引用死链；用 shallowRef。
3. **定时器 ID 被覆盖**：多次 setInterval 最后只能 clear 最后一个；集中管理。
4. **组件卸载 API 选错**：keepAlive 下用 `onDeactivated`，不是 `onBeforeUnmount`。
5. **Worker 未 terminate**：独立线程继续跑，持有 postMessage 的引用链。

## 相关

- 事件清理 → [event/cleanup](../event/cleanup.md)
- 内存泄漏总览 → [perf/memory-leaks](../perf/memory-leaks.md)
- Vue / React 卸载 → [vue-react-dispose](vue-react-dispose.md)
