cleanup
本文回答:事件清理 ——
off / once / clear以及组件卸载时的生命周期管理。
一、手动 off
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
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:
mapcadLayer.eventBus.clear(); // ⚠️ 无参——清空【所有】事件的【所有】handler
// eventBus.clear('entity:added') 不传参不起过滤作用:参数被忽略,照样全清
// 清某事件的所有 handler 没有公开 API,只能逐个 off
map(vjmap 原生)没提供 clear;手动管理:
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
import { onBeforeUnmount } from 'vue';
const handler = () => { ... };
mapcadLayer.eventBus.on(VjmapextEvents.EntityAdded, handler);
onBeforeUnmount(() => {
mapcadLayer.eventBus.off(VjmapextEvents.EntityAdded, handler);
});
React
useEffect(() => {
const handler = () => { ... };
mapcadLayer.eventBus.on(VjmapextEvents.EntityAdded, handler);
return () => mapcadLayer.eventBus.off(VjmapextEvents.EntityAdded, handler);
}, []);
五、AbortController(现代方案)
vjmap / vjmapext 原生不支持 AbortSignal,但可以封装:
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()。
七、典型泄漏模式
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)须重新订阅。
销毁顺序:
mapcadLayer.eventBus.clear();
map.removeControl(mapcadLayer);
map.remove();
container.innerHTML = '';
九、DOM 事件
对于添加到 map.getContainer() 上的自定义 DOM 事件(比如右键菜单):
const container = map.getContainer();
const ctxMenu = e => { ... };
container.addEventListener('contextmenu', ctxMenu);
// 卸载
container.removeEventListener('contextmenu', ctxMenu);
十、内存泄漏自查
浏览器 DevTools → Memory → Heap Snapshot:
- 多次打开 / 关闭 map,看 map 实例数是否一直涨
- 搜
MapCadLayer看有没有悬挂引用 - 搜 handler 闭包里的对象名
十一、常见坑
- 箭头函数 inline → 无法 off;必须保存引用。
- 多次 setup 重复绑:每次调用 setup 都绑一份;必须先 off 或 guard。
- vue ref 未被捕获:setup 里的变量在卸载后仍被回调引用,导致 Vue 实例无法 GC。
- eventBus.clear() 太粗:会清除所有订阅(包括 vjmapext 内部需要的);一般不推荐,除非销毁整个 mapcadLayer。
map.remove()后仍 on:会抛错 / 静默;map._removed可判断。
相关
- 事件总览 → overview
- 性能:内存泄漏 → perf/memory-leaks
- Vue/React 卸载 → trouble/vue-react-dispose