diff --git a/common/changes/@visactor/vrender-animate/fix-fix-memory-leaks_2026-03-03-11-41.json b/common/changes/@visactor/vrender-animate/fix-fix-memory-leaks_2026-03-03-11-41.json new file mode 100644 index 000000000..404d3eaa9 --- /dev/null +++ b/common/changes/@visactor/vrender-animate/fix-fix-memory-leaks_2026-03-03-11-41.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix memory leaks\n\n", + "type": "none", + "packageName": "@visactor/vrender-animate" + } + ], + "packageName": "@visactor/vrender-animate", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-components/fix-fix-memory-leaks_2026-03-03-11-41.json b/common/changes/@visactor/vrender-components/fix-fix-memory-leaks_2026-03-03-11-41.json new file mode 100644 index 000000000..6a59ed89a --- /dev/null +++ b/common/changes/@visactor/vrender-components/fix-fix-memory-leaks_2026-03-03-11-41.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix memory leaks\n\n", + "type": "none", + "packageName": "@visactor/vrender-components" + } + ], + "packageName": "@visactor/vrender-components", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/common/changes/@visactor/vrender-core/fix-fix-memory-leaks_2026-03-03-11-41.json b/common/changes/@visactor/vrender-core/fix-fix-memory-leaks_2026-03-03-11-41.json new file mode 100644 index 000000000..cd9e72814 --- /dev/null +++ b/common/changes/@visactor/vrender-core/fix-fix-memory-leaks_2026-03-03-11-41.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: fix memory leaks\n\n", + "type": "none", + "packageName": "@visactor/vrender-core" + } + ], + "packageName": "@visactor/vrender-core", + "email": "lixuef1313@163.com" +} \ No newline at end of file diff --git a/packages/vrender-components/src/data-zoom/interaction.ts b/packages/vrender-components/src/data-zoom/interaction.ts index 96258ac09..98a650be1 100644 --- a/packages/vrender-components/src/data-zoom/interaction.ts +++ b/packages/vrender-components/src/data-zoom/interaction.ts @@ -119,7 +119,7 @@ export class DataZoomInteraction extends EventEmitter { } clearVGlobalEvents() { - (vglobal.env === 'browser' ? vglobal : this.stage).addEventListener('touchmove', this._handleTouchMove, { + (vglobal.env === 'browser' ? vglobal : this.stage).removeEventListener('touchmove', this._handleTouchMove, { passive: false }); } diff --git a/packages/vrender-components/src/player/base-player.ts b/packages/vrender-components/src/player/base-player.ts index 9e1656d25..ce7f5db31 100644 --- a/packages/vrender-components/src/player/base-player.ts +++ b/packages/vrender-components/src/player/base-player.ts @@ -390,4 +390,11 @@ export abstract class BasePlayer extends AbstractComponent> { * 浏览器上的事件必须解绑,防止内存泄漏,场景树上的事件会自动解绑 */ super.release(all); - (vglobal.env === 'browser' ? vglobal : this.stage).addEventListener('touchmove', this._handleTouchMove, { + (vglobal.env === 'browser' ? vglobal : this.stage).removeEventListener('touchmove', this._handleTouchMove, { passive: false }); this._clearAllDragEvents(); diff --git a/packages/vrender-core/src/common/event-listener-manager.ts b/packages/vrender-core/src/common/event-listener-manager.ts index 036ac309f..1b370f211 100644 --- a/packages/vrender-core/src/common/event-listener-manager.ts +++ b/packages/vrender-core/src/common/event-listener-manager.ts @@ -7,9 +7,15 @@ import type { IEventListenerManager } from '../interface/event-listener-manager' export class EventListenerManager implements IEventListenerManager { /** * Map that stores the mapping from original listeners to wrapped listeners - * Structure: Map> + * Structure: Map>> */ - protected _listenerMap: Map>; + protected _listenerMap: Map< + string, + Map< + EventListenerOrEventListenerObject, + Map + > + >; /** * Transformer function that transforms the event @@ -44,6 +50,16 @@ export class EventListenerManager implements IEventListenerManager { return; } + const capture = this._resolveCapture(options); + const once = this._resolveOnce(options); + const listenerTypeMap = this._getOrCreateListenerTypeMap(type); + const wrappedMap = this._getOrCreateWrappedMap(listenerTypeMap, listener); + + // Align with native behavior: adding same (type, listener, capture) repeatedly is a no-op. + if (wrappedMap.has(capture)) { + return; + } + // Create a wrapped listener that applies the transformation const wrappedListener = (event: Event) => { const transformedEvent = this._eventListenerTransformer(event); @@ -52,13 +68,15 @@ export class EventListenerManager implements IEventListenerManager { } else if (listener.handleEvent) { listener.handleEvent(transformedEvent); } + + // Native once listeners are removed automatically after dispatch, clear the mapping as well. + if (once) { + this._deleteListenerRecord(type, listener, capture); + } }; // Store the mapping between original and wrapped listener - if (!this._listenerMap.has(type)) { - this._listenerMap.set(type, new Map()); - } - this._listenerMap.get(type)!.set(listener, wrappedListener); + wrappedMap.set(capture, { wrappedListener, options }); // Add the wrapped listener this._nativeAddEventListener(type, wrappedListener, options); @@ -79,17 +97,12 @@ export class EventListenerManager implements IEventListenerManager { return; } - // Get the wrapped listener from our map - const wrappedListener = this._listenerMap.get(type)?.get(listener); - if (wrappedListener) { + const capture = this._resolveCapture(options); + const wrappedRecord = this._listenerMap.get(type)?.get(listener)?.get(capture); + if (wrappedRecord) { // Remove the wrapped listener - this._nativeRemoveEventListener(type, wrappedListener, options); - - // Remove from our map - this._listenerMap.get(type)!.delete(listener); - if (this._listenerMap.get(type)!.size === 0) { - this._listenerMap.delete(type); - } + this._nativeRemoveEventListener(type, wrappedRecord.wrappedListener, capture); + this._deleteListenerRecord(type, listener, capture); } } @@ -105,14 +118,74 @@ export class EventListenerManager implements IEventListenerManager { * Clear all event listeners */ clearAllEventListeners(): void { - this._listenerMap.forEach((listenersMap, type) => { - listenersMap.forEach((wrappedListener, originalListener) => { - this._nativeRemoveEventListener(type, wrappedListener, undefined); + this._listenerMap.forEach((listenerMap, type) => { + listenerMap.forEach(wrappedMap => { + wrappedMap.forEach((wrappedRecord, capture) => { + this._nativeRemoveEventListener(type, wrappedRecord.wrappedListener, capture); + }); }); }); this._listenerMap.clear(); } + protected _resolveCapture(options?: boolean | EventListenerOptions | AddEventListenerOptions): boolean { + if (typeof options === 'boolean') { + return options; + } + return !!options?.capture; + } + + protected _resolveOnce(options?: boolean | AddEventListenerOptions): boolean { + return typeof options === 'object' && !!options?.once; + } + + protected _getOrCreateListenerTypeMap( + type: string + ): Map< + EventListenerOrEventListenerObject, + Map + > { + let listenerTypeMap = this._listenerMap.get(type); + if (!listenerTypeMap) { + listenerTypeMap = new Map(); + this._listenerMap.set(type, listenerTypeMap); + } + return listenerTypeMap; + } + + protected _getOrCreateWrappedMap( + listenerTypeMap: Map< + EventListenerOrEventListenerObject, + Map + >, + listener: EventListenerOrEventListenerObject + ): Map { + let wrappedMap = listenerTypeMap.get(listener); + if (!wrappedMap) { + wrappedMap = new Map(); + listenerTypeMap.set(listener, wrappedMap); + } + return wrappedMap; + } + + protected _deleteListenerRecord(type: string, listener: EventListenerOrEventListenerObject, capture: boolean): void { + const listenerTypeMap = this._listenerMap.get(type); + if (!listenerTypeMap) { + return; + } + const wrappedMap = listenerTypeMap.get(listener); + if (!wrappedMap) { + return; + } + wrappedMap.delete(capture); + if (wrappedMap.size === 0) { + listenerTypeMap.delete(listener); + } + if (listenerTypeMap.size === 0) { + this._listenerMap.delete(type); + } + } + /** * Native implementation of addEventListener * To be implemented by derived classes diff --git a/packages/vrender-core/src/event/event-manager.ts b/packages/vrender-core/src/event/event-manager.ts index 9f58b20a5..18214de14 100644 --- a/packages/vrender-core/src/event/event-manager.ts +++ b/packages/vrender-core/src/event/event-manager.ts @@ -790,6 +790,13 @@ export class EventManager { const constructor = event.constructor; if (!this.eventPool.has(constructor as any)) { + this.eventPool.get(constructor as any).forEach(e => { + e.eventPhase = event.NONE; + e.currentTarget = null; + e.path = []; + e.detailPath = []; + e.target = null; + }); this.eventPool.set(constructor as any, []); } diff --git a/packages/vrender-core/src/graphic/graphic.ts b/packages/vrender-core/src/graphic/graphic.ts index 7283c99b4..bf48f41ea 100644 --- a/packages/vrender-core/src/graphic/graphic.ts +++ b/packages/vrender-core/src/graphic/graphic.ts @@ -1582,6 +1582,7 @@ export abstract class Graphic = Partial