fix(core): 节点销毁时移除其事件监听

迭代容器等场景会反复重建节点,此前 destroy 只清理事件队列,
事件监听仍留在 EventHelper 上导致泄漏。新增 removeNodeEventsOf,
在 Node.destroy 时按节点移除已绑定的监听
This commit is contained in:
roymondchen 2026-10-30 16:47:23 +08:00
parent 2f8a504163
commit 522a0068f5
3 changed files with 89 additions and 0 deletions

View File

@ -55,6 +55,7 @@ export default class EventHelper extends EventEmitter {
public eventQueue: EventCache[] = [];
private nodeEventList = new Map<(fromCpt: TMagicNode, ...args: any[]) => void, symbol>();
private nodeEventHandlers = new WeakMap<TMagicNode, ((fromCpt: TMagicNode, ...args: any[]) => void)[]>();
private dataSourceEventList = new Map<string, Map<string, (...args: any[]) => void>>();
private beforeEventHandler?: BeforeEventHandler;
private afterEventHandler?: AfterEventHandler;
@ -125,6 +126,13 @@ export default class EventHelper extends EventEmitter {
this.nodeEventList.set(eventHandler, eventName);
this.on(eventName, eventHandler);
const handlers = this.nodeEventHandlers.get(node);
if (handlers) {
handlers.push(eventHandler);
} else {
this.nodeEventHandlers.set(node, [eventHandler]);
}
});
}
@ -135,6 +143,25 @@ export default class EventHelper extends EventEmitter {
}
this.nodeEventList.clear();
this.nodeEventHandlers = new WeakMap();
}
/**
* 移除单个节点绑定的事件,节点销毁时调用,避免迭代容器等场景反复重建节点导致监听泄漏
*/
public removeNodeEventsOf(node: TMagicNode) {
const handlers = this.nodeEventHandlers.get(node);
if (!handlers) {
return;
}
for (const handler of handlers) {
const name = this.nodeEventList.get(handler);
name && this.off(name, handler);
this.nodeEventList.delete(handler);
}
this.nodeEventHandlers.delete(node);
}
public bindDataSourceEvents() {

View File

@ -149,6 +149,7 @@ class Node extends EventEmitter {
}
public destroy() {
this.app?.eventHelper?.removeNodeEventsOf(this);
this.eventQueue.length = 0;
this.instance = null;
this.events = [];

View File

@ -187,6 +187,67 @@ describe('EventHelper - bindNodeEvents / initEvents / removeNodeEvents', () => {
helper.removeNodeEvents();
expect((helper as any).nodeEventList.size).toBe(0);
});
test('removeNodeEventsOf 只移除指定节点的监听,未绑定过的节点安全返回', () => {
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [
{ id: 'btn_1', type: 'button', events: [{ name: 'click', actions: [] }] },
{ id: 'btn_2', type: 'button', events: [{ name: 'click', actions: [] }] },
{ id: 'btn_3', type: 'button' },
],
},
],
} as any),
});
const helper = app.eventHelper!;
const node1 = app.getNode('btn_1')!;
const node2 = app.getNode('btn_2')!;
const symbol1 = node1.eventKeys.get('click_btn_1')!;
const symbol2 = node2.eventKeys.get('click_btn_2')!;
expect((helper as any).nodeEventList.size).toBe(2);
expect(() => helper.removeNodeEventsOf(app.getNode('btn_3')!)).not.toThrow();
expect((helper as any).nodeEventList.size).toBe(2);
helper.removeNodeEventsOf(node1);
expect((helper as any).nodeEventList.size).toBe(1);
expect(helper.listenerCount(symbol1)).toBe(0);
expect(helper.listenerCount(symbol2)).toBe(1);
// 重复调用无副作用
helper.removeNodeEventsOf(node1);
expect((helper as any).nodeEventList.size).toBe(1);
});
test('节点 destroy 时会移除其在 eventHelper 上的监听', () => {
const app = new App({
config: createDsl({
items: [
{
type: NodeType.PAGE,
id: 'page_1',
items: [{ id: 'btn_1', type: 'button', events: [{ name: 'click', actions: [] }] }],
},
],
} as any),
});
const helper = app.eventHelper!;
const node = app.getNode('btn_1')!;
const symbol = node.eventKeys.get('click_btn_1')!;
expect(helper.listenerCount(symbol)).toBe(1);
node.destroy();
expect(helper.listenerCount(symbol)).toBe(0);
expect((helper as any).nodeEventList.size).toBe(0);
});
});
describe('EventHelper - 事件队列', () => {