唯杰地图 logo唯杰地图
VJMAP
EN

事件泄漏 / 内存涨 / 回调反复触发

vjcad/references/trouble/event-leak.md 6.3 KB171 行
原文

文档: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