diff --git a/.changeset/small-experts-hug.md b/.changeset/small-experts-hug.md new file mode 100644 index 000000000..a9f517e63 --- /dev/null +++ b/.changeset/small-experts-hug.md @@ -0,0 +1,5 @@ +--- +"@qiankunjs/sandbox": patch +--- + +feat: support addEventListener with once options to avoid memory leak diff --git a/packages/sandbox/src/patchers/windowListener.ts b/packages/sandbox/src/patchers/windowListener.ts index ce4bebee7..242bf5da9 100644 --- a/packages/sandbox/src/patchers/windowListener.ts +++ b/packages/sandbox/src/patchers/windowListener.ts @@ -7,35 +7,104 @@ const rawAddEventListener = window.addEventListener; const rawRemoveEventListener = window.removeEventListener; +type ListenerMapObject = { + listener: EventListenerOrEventListenerObject; + options: AddEventListenerOptions; + rawListener: EventListenerOrEventListenerObject; +}; + +const DEFAULT_OPTIONS: AddEventListenerOptions = { capture: false, once: false, passive: false }; + +// 移除cacheListener +const removeCacheListener = ( + listenerMap: Map, + type: string, + rawListener: EventListenerOrEventListenerObject, + rawOptions?: boolean | AddEventListenerOptions, +): ListenerMapObject => { + // 如果 options 为 null、undefined,使用默认值 + const opts = rawOptions ?? DEFAULT_OPTIONS; + // 处理 options,确保它是一个对象 + const options = typeof opts === 'object' ? opts : { capture: !!opts }; + + const cachedTypeListeners = listenerMap.get(type) || []; + // listener和capture/useCapture都相同,认为是同一个监听 + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener + const findIndex = cachedTypeListeners.findIndex( + (item) => item.rawListener === rawListener && item.options.capture == options.capture, + ); + if (findIndex > -1) { + const cacheListener = cachedTypeListeners[findIndex]; + cachedTypeListeners.splice(findIndex, 1); + return cacheListener; + } + + // 返回原始listener和options + return { listener: rawListener, rawListener, options }; +}; + +// 添加监听构造一个cacheListener对象,考虑到多次添加同一个监听和once的情况 +const addCacheListener = ( + listenerMap: Map, + type: string, + rawListener: EventListenerOrEventListenerObject, + rawOptions?: boolean | AddEventListenerOptions, +): ListenerMapObject | undefined => { + // 如果 options 为 null、undefined,使用默认值 + const opts = rawOptions ?? DEFAULT_OPTIONS; + // 处理 options,确保它是一个对象 + const options = typeof opts === 'object' ? opts : { capture: !!opts }; + + const cachedTypeListeners = listenerMap.get(type) || []; + // listener和capture/useCapture都相同,认为是同一个监听 + // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + const findIndex = cachedTypeListeners.findIndex( + (item) => item.rawListener === rawListener && item.options.capture == options.capture, + ); + // 如果事件已经添加到了target的event listeners 列表中,直接返回不需要添加第二次 + if (findIndex > -1) return; + + let listener: EventListenerOrEventListenerObject = rawListener; + if (options.once) + listener = (event: Event) => { + (rawListener as EventListener)(event); + removeCacheListener(listenerMap, type, rawListener, options); + }; + const cacheListener = { listener, options, rawListener }; + listenerMap.set(type, [...cachedTypeListeners, cacheListener]); + return cacheListener; +}; + export default function patch(global: WindowProxy) { - const listenerMap = new Map(); + const listenerMap = new Map(); global.addEventListener = ( type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, + rawListener: EventListenerOrEventListenerObject, + rawOptions?: boolean | AddEventListenerOptions, ) => { - const listeners = listenerMap.get(type) || []; - listenerMap.set(type, [...listeners, listener]); + const addListener = addCacheListener(listenerMap, type, rawListener, rawOptions); + // 如果返回空,则代表事件已经添加过了,不需要重复添加 + if (!addListener) return; + const { listener, options } = addListener; return rawAddEventListener.call(window, type, listener, options); }; global.removeEventListener = ( type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, + rawListener: EventListenerOrEventListenerObject, + rawOptions?: boolean | AddEventListenerOptions, ) => { - const storedTypeListeners = listenerMap.get(type); - if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) !== -1) { - storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1); - } + const { listener, options } = removeCacheListener(listenerMap, type, rawListener, rawOptions); return rawRemoveEventListener.call(window, type, listener, options); }; return function free() { listenerMap.forEach((listeners, type) => - [...listeners].forEach((listener) => global.removeEventListener(type, listener)), + [...listeners].forEach(({ rawListener, options }) => global.removeEventListener(type, rawListener, options)), ); + // 清空listenerMap,避免listenerMap中还存有listener导致内存泄漏 + listenerMap.clear(); global.addEventListener = rawAddEventListener; global.removeEventListener = rawRemoveEventListener;