# 事件泄漏 / 内存涨 / 回调反复触发

> 文档：`vjcad/doc/event/event-usage.md`、`vjcad/doc/plugin/plugin-context.md`
> 相关：`event/listen.md`、`plugin/context-api.md`

## 1. 现象

- 切换路由后内存稳步上涨（DevTools Memory 标签 → "Detached HTMLElement"）
- 同一事件 `EntityAdded` 触发了好几次（`message.info` 弹了 3 个一样的 toast）
- 关闭 dialog 后仍能"看到 dialog 的副作用"
- 旧版本组件的回调还在跑

根因几乎都是 **监听没有清理**。

## 2. 三类监听的清理方式

| 监听 | 注册 | 清理 |
|---|---|---|
| `CadEventManager` 全局事件 | `events.on(EventType, fn)` | `events.off(EventType, fn)` |
| 实体修改 | ❌ 实体级 `entity.on('modified')` 不存在——走 `CadEvents.EntityModified` 按 `args.entity` 过滤 | 同上 off |
| 命令系统 / Reactor | `Engine.reactorManager.registerReactor(r, docId?)` | `unregisterReactor(r.id, docId?)` |
| DOM 监听（含浮动工具栏） | `addEventListener` | `removeEventListener` |

注意 **闭包**：`events.on(t, () => { ... })` 写了匿名函数后 **没法 off**，必须存引用：

```js
// ❌ 错：无法清理
events.on(CadEvents.EntityAdded, args => console.log(args.entity.type));

// ✅ 对
const handler = args => console.log(args.entity.type);
events.on(CadEvents.EntityAdded, handler);
// 卸载时
events.off(CadEvents.EntityAdded, handler);
```

## 3. React / Vue 中的标准模式

### React

```jsx
useEffect(() => {
    const handler = args => console.log('selection changed', args.currentSelection);
    Engine.eventManager.on(CadEvents.SelectionChanged, handler);

    return () => {
        Engine.eventManager.off(CadEvents.SelectionChanged, handler);
    };
}, []);
```

### Vue 3

```js
const handler = args => console.log(args);
onMounted(() => Engine.eventManager.on(CadEvents.SelectionChanged, handler));
onBeforeUnmount(() => Engine.eventManager.off(CadEvents.SelectionChanged, handler));
```

## 4. 用插件 `PluginContext` 自动管理

vjcad 的 `PluginContext` 提供 **自动 cleanup** 的事件 API：

```js
// plugin onActivate
context.on(CadEvents.EntityAdded, handler);

// 插件 deactivate / unload 时 vjcad 自动 off
```

但这只对 **该插件的生命周期** 自动；如果你在插件外用 `events.on`，仍然要自己 off。

## 5. `useEffect` 重渲染问题

```jsx
// ❌ 错：每次 props 变都重绑一次，最终 N 个监听器
useEffect(() => {
    Engine.eventManager.on(CadEvents.EntityAdded, args => {
        if (args.entity.type === props.type) doSth();
    });
});

// ✅ 对：依赖固定 + cleanup
useEffect(() => {
    const handler = args => {
        if (args.entity.type === propsRef.current.type) doSth();
    };
    Engine.eventManager.on(CadEvents.EntityAdded, handler);
    return () => Engine.eventManager.off(CadEvents.EntityAdded, handler);
}, []);
```

需要在回调里读最新 props，用 `useRef` 接住，避免重绑。

## 6. 检测当前监听数

```js
const events = CadEventManager.getInstance();
// CadEventManager 上没有 _listeners 字段（监听存在私有 emitter 的 Map 里），用公开的 listenerCount
console.log('当前 EntityAdded 监听数:', events.listenerCount(CadEvents.EntityAdded));
```

如果数字会随路由 / 组件切换 **增长**，说明你漏了 `off`。

## 7. 实体修改监听

`EntityBase` **没有** `.on/.off`——实体不是事件发射器（`ReactorEvent.Modified` 枚举是 reactor 回调体系用，不是实体方法）。监听实体修改走全局事件总线，按实体过滤：

```js
const ent = new LineEnt(...);
Engine.addEntities(ent);
const cb = args => { if (args.entity === ent) console.log('modified', args); };
events.on(CadEvents.EntityModified, cb);   // 必须注册 cb 本身——off 按引用移除，包一层箭头函数就 off 不掉

// 删除实体前先退订，否则闭包持有 ent 引用不释放
Engine.eraseEntities(ent);
events.off(CadEvents.EntityModified, cb);
```

## 8. 浮动工具栏 / 自定义对话框

```js
const tb = createFloatingToolbar('my', { ... });
// 内部添加了 'mousedown' / 'click' 监听
// 销毁要：
tb.destroy();              // ✅ vjcad 内部会清掉自己的事件
```

如果在 `onClick` 回调里 **再注册**了 `events.on` —— 那部分要自己 cleanup。

## 9. 一致命令多次 `regist`

```js
// ❌ 每次组件 mount 都注册一次 → items 里堆积同名定义，且执行的始终是第一次注册的那个
useEffect(() => {
    CommandRegistry.regist(new CommandDefinition('MYCMD', '我', MyCmd, opt));
});
```

`regist` 只 push 不去重：执行时 `CommandRegistry.item(name)` 取**第一个**同名定义（先注册者胜出），后注册的新类/新闭包根本不生效，而 `items` 越积越多。要替换命令须先 `CommandRegistry.unregist(name)`（每次只删第一个匹配）再 `regist`。

正确：

```js
useEffect(() => {
    CommandRegistry.regist(new CommandDefinition('MYCMD', '我', MyCmd, opt));
}, []);   // 只注册一次
```

## 10. 内存检查清单（生产前必走）

- [ ] 路由切走 → DevTools Memory snapshot → 看 `MainView` 实例数应保持 1
- [ ] 反复 mount / unmount 一个对话框 → 看 `events.listenerCount(...)` 不增长
- [ ] 切换路由 → 老路由的 `Engine` 是否 `dispose`
- [ ] 浮动工具栏 / 侧边栏 / 自定义模态都做了清理
- [ ] 业务里 `events.on` 都有对应 `off`

## 红旗

- **匿名函数无法 off**：所有 `events.on / .addEventListener` 都要保引用
- 在 `useEffect` 里没写 cleanup → 几乎一定会泄漏
- `onActivate` / `onDeactivate` 不对称 → 插件激活两次会注册两次监听
- 实体没有 `on/off`（见 §7）：监听实体修改统一走全局 `CadEvents.EntityModified` 并配对 `off`——vjcad 不会自动清理
- DevTools 中 "Listeners" 一栏可以快速定位某个 DOM / 全局对象上挂了多少回调
- 若用 React StrictMode（开发环境会双调用 useEffect），cleanup 写对了不会有问题；写错时会立刻看到双倍监听

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/event/event-usage.md`、`vjcad/doc/plugin/plugin-context.md`
- 相关：`event/listen.md`、`plugin/context-api.md`
