事件泄漏 / 内存涨 / 回调反复触发
文档:
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,必须存引用:
// ❌ 错:无法清理
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
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
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:
// plugin onActivate
context.on(CadEvents.EntityAdded, handler);
// 插件 deactivate / unload 时 vjcad 自动 off
但这只对 该插件的生命周期 自动;如果你在插件外用 events.on,仍然要自己 off。
5. useEffect 重渲染问题
// ❌ 错:每次 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. 检测当前监听数
const events = CadEventManager.getInstance();
// CadEventManager 上没有 _listeners 字段(监听存在私有 emitter 的 Map 里),用公开的 listenerCount
console.log('当前 EntityAdded 监听数:', events.listenerCount(CadEvents.EntityAdded));
如果数字会随路由 / 组件切换 增长,说明你漏了 off。
7. 实体修改监听
EntityBase 没有 .on/.off——实体不是事件发射器(ReactorEvent.Modified 枚举是 reactor 回调体系用,不是实体方法)。监听实体修改走全局事件总线,按实体过滤:
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. 浮动工具栏 / 自定义对话框
const tb = createFloatingToolbar('my', { ... });
// 内部添加了 'mousedown' / 'click' 监听
// 销毁要:
tb.destroy(); // ✅ vjcad 内部会清掉自己的事件
如果在 onClick 回调里 再注册了 events.on —— 那部分要自己 cleanup。
9. 一致命令多次 regist
// ❌ 每次组件 mount 都注册一次 → items 里堆积同名定义,且执行的始终是第一次注册的那个
useEffect(() => {
CommandRegistry.regist(new CommandDefinition('MYCMD', '我', MyCmd, opt));
});
regist 只 push 不去重:执行时 CommandRegistry.item(name) 取第一个同名定义(先注册者胜出),后注册的新类/新闭包根本不生效,而 items 越积越多。要替换命令须先 CommandRegistry.unregist(name)(每次只删第一个匹配)再 regist。
正确:
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