唯杰地图 logo唯杰地图
VJMAP
EN

cleanup

vjmap/references/event/cleanup.md 5.1 KB168 行
原文

本文回答:事件清理 —— 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 闭包里的对象名

十一、常见坑

  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 可判断。

相关