> **本文回答**：事件清理 —— `off / once / clear` 以及组件卸载时的生命周期管理。

## 一、手动 off

```js
const handler = e => { ... };

map.on('click', handler);
map.off('click', handler);

mapcadLayer.eventBus.on(VjmapextEvents.EntityAdded, handler);
mapcadLayer.eventBus.off(VjmapextEvents.EntityAdded, handler);
```

**匿名函数无法 off**。保留引用。

## 二、once

```js
map.once('load', () => console.log('loaded'));
mapcadLayer.eventBus.once(VjmapextEvents.CommandEnded, ({ name }) => ...);
```

触发一次后自动解绑。`map.once(type)` 不传 listener 时返回 Promise（`await map.once('idle')`）；图层级 `map.once(type, layerId, fn)` 在第一次该类事件时就被消耗，即使没命中图层（fn 不会被调用）。

## 三、批量清理

eventBus：

```js
mapcadLayer.eventBus.clear();          // ⚠️ 无参——清空【所有】事件的【所有】handler
// eventBus.clear('entity:added') 不传参不起过滤作用：参数被忽略，照样全清
// 清某事件的所有 handler 没有公开 API，只能逐个 off
```

map（vjmap 原生）没提供 clear；手动管理：

```js
class MapListeners {
  constructor(map) { this.map = map; this.list = []; }
  on(type, layerId, handler) {
    if (typeof layerId === 'function') { handler = layerId; layerId = null; }
    if (layerId) this.map.on(type, layerId, handler);
    else this.map.on(type, handler);
    this.list.push({ type, layerId, handler });
  }
  clear() {
    this.list.forEach(({ type, layerId, handler }) => {
      if (layerId) this.map.off(type, layerId, handler);
      else this.map.off(type, handler);
    });
    this.list = [];
  }
}

const ml = new MapListeners(map);
ml.on('click', 'layer1', h1);
ml.on('mousemove', h2);

// 组件卸载
ml.clear();
```

## 四、Vue / React 组件里的典型做法

### Vue 3

```js
import { onBeforeUnmount } from 'vue';

const handler = () => { ... };
mapcadLayer.eventBus.on(VjmapextEvents.EntityAdded, handler);
onBeforeUnmount(() => {
  mapcadLayer.eventBus.off(VjmapextEvents.EntityAdded, handler);
});
```

### React

```jsx
useEffect(() => {
  const handler = () => { ... };
  mapcadLayer.eventBus.on(VjmapextEvents.EntityAdded, handler);
  return () => mapcadLayer.eventBus.off(VjmapextEvents.EntityAdded, handler);
}, []);
```

## 五、AbortController（现代方案）

vjmap / vjmapext 原生不支持 AbortSignal，但可以封装：

```js
function onWithSignal(target, event, handler, signal) {
  target.on(event, handler);
  signal?.addEventListener('abort', () => target.off(event, handler));
}

const ac = new AbortController();
onWithSignal(map, 'click', h, ac.signal);

// 组件卸载
ac.abort();
```

## 六、map.remove() 会清吗

`map.remove()` 会**清除所有内部监听**；自定义的用户监听不会被自动 off（避免副作用），还是建议手动 off 或 `eventBus.clear()`。

## 七、典型泄漏模式

```js
function setup() {
  map.on('click', e => heavyHandler(e));          // 每次新建闭包 → 真的叠加
  map.on('click', 'my-layer', heavyHandler);      // 图层级每次新建 delegate → 同一 fn 也叠加
}
setup(); setup(); setup();   // 点一次 fire 3 次（两种各 3 次）
```

同一函数引用重复 `map.on(type, fn)` 会被去重（只触发一次）；真正累积的是每次新建的闭包，以及 `map.on(type, layerId, fn)`（同一 fn 也会叠加，一次 `map.off(type, layerId, fn)` 只摘掉一个）。解决：保存引用，避免重复绑；或者绑前先 `off`。

## 八、事件总线生命周期

`mapcadLayer.eventBus` 随 `mapcadLayer` 一起销毁。`map.removeControl(mapcadLayer)` 或 `map.remove()` 都会调 `onRemove`，其内部 `eventBus.clear()` **连外部订阅一起清空**；之后复用该 mapcadLayer（重新 addControl）须重新订阅。

销毁顺序：

```js
mapcadLayer.eventBus.clear();
map.removeControl(mapcadLayer);
map.remove();
container.innerHTML = '';
```

## 九、DOM 事件

对于添加到 `map.getContainer()` 上的自定义 DOM 事件（比如右键菜单）：

```js
const container = map.getContainer();
const ctxMenu = e => { ... };
container.addEventListener('contextmenu', ctxMenu);

// 卸载
container.removeEventListener('contextmenu', ctxMenu);
```

## 十、内存泄漏自查

浏览器 DevTools → Memory → Heap Snapshot：

- 多次打开 / 关闭 map，看 map 实例数是否一直涨
- 搜 `MapCadLayer` 看有没有悬挂引用
- 搜 handler 闭包里的对象名

## 十一、常见坑

1. **箭头函数 inline** → 无法 off；必须保存引用。
2. **多次 setup 重复绑**：每次调用 setup 都绑一份；必须先 off 或 guard。
3. **vue ref 未被捕获**：setup 里的变量在卸载后仍被回调引用，导致 Vue 实例无法 GC。
4. **eventBus.clear() 太粗**：会清除所有订阅（包括 vjmapext 内部需要的）；一般不推荐，除非销毁整个 mapcadLayer。
5. **`map.remove()` 后仍 on**：会抛错 / 静默；`map._removed` 可判断。

## 相关

- 事件总览 → [overview](overview.md)
- 性能：内存泄漏 → [perf/memory-leaks](../perf/memory-leaks.md)
- Vue/React 卸载 → [trouble/vue-react-dispose](../trouble/vue-react-dispose.md)
