From 522a0068f5807ec2212e6ca7e87b96891594b3fc Mon Sep 17 00:00:00 2001 From: roymondchen Date: Fri, 30 Oct 2026 16:47:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E8=8A=82=E7=82=B9=E9=94=80?= =?UTF-8?q?=E6=AF=81=E6=97=B6=E7=A7=BB=E9=99=A4=E5=85=B6=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迭代容器等场景会反复重建节点,此前 destroy 只清理事件队列, 事件监听仍留在 EventHelper 上导致泄漏。新增 removeNodeEventsOf, 在 Node.destroy 时按节点移除已绑定的监听 --- packages/core/src/EventHelper.ts | 27 +++++++++++ packages/core/src/Node.ts | 1 + packages/core/tests/EventHelper.spec.ts | 61 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/packages/core/src/EventHelper.ts b/packages/core/src/EventHelper.ts index 0e4c926a..df96a808 100644 --- a/packages/core/src/EventHelper.ts +++ b/packages/core/src/EventHelper.ts @@ -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 void)[]>(); private dataSourceEventList = new Map 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() { diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts index e5d29fc8..72607436 100644 --- a/packages/core/src/Node.ts +++ b/packages/core/src/Node.ts @@ -149,6 +149,7 @@ class Node extends EventEmitter { } public destroy() { + this.app?.eventHelper?.removeNodeEventsOf(this); this.eventQueue.length = 0; this.instance = null; this.events = []; diff --git a/packages/core/tests/EventHelper.spec.ts b/packages/core/tests/EventHelper.spec.ts index 2987f2e9..1abf7708 100644 --- a/packages/core/tests/EventHelper.spec.ts +++ b/packages/core/tests/EventHelper.spec.ts @@ -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 - 事件队列', () => {