Skip to content

Commit a9460d0

Browse files
arturovtAndrewKushnir
authored andcommitted
fix(zone.js): remove abort listener on a signal when actual event is removed (angular#55339)
This commit updates the implementation of the `addEventListener` patcher. We're currently creating an abort event listener on the signal (when it's provided) and never remove it. The abort event listener creates a closure which captures `task` (tasks capture zones and other stuff too) and prevent `task`, zones and signals from being garbage collected. We now store the function which removes the abort event listener when the actual event listener is being removed. The function is stored on task data since task data is already being used to store different types of information that's necessary to be shared between `addEventListener` and `removeEventListener`. Closes angular#54739 PR Close angular#55339
1 parent afc057e commit a9460d0

File tree

1 file changed

+23
-16
lines changed

1 file changed

+23
-16
lines changed

packages/zone.js/lib/common/events.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,7 @@ export function patchEventTarget(
427427
const passive =
428428
passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1;
429429
const options = buildEventListenerOptions(arguments[2], passive);
430-
const signal =
431-
options &&
432-
typeof options === 'object' &&
433-
options.signal &&
434-
typeof options.signal === 'object'
435-
? options.signal
436-
: undefined;
430+
const signal: AbortSignal | undefined = options?.signal;
437431
if (signal?.aborted) {
438432
// the signal is an aborted one, just return without attaching the event listener.
439433
return;
@@ -528,14 +522,18 @@ export function patchEventTarget(
528522
if (signal) {
529523
// after task is scheduled, we need to store the signal back to task.options
530524
taskData.options.signal = signal;
531-
nativeListener.call(
532-
signal,
533-
'abort',
534-
() => {
535-
task.zone.cancelTask(task);
536-
},
537-
{once: true},
538-
);
525+
// Wrapping `task` in a weak reference would not prevent memory leaks. Weak references are
526+
// primarily used for preventing strong references cycles. `onAbort` is always reachable
527+
// as it's an event listener, so its closure retains a strong reference to the `task`.
528+
const onAbort = () => task.zone.cancelTask(task);
529+
nativeListener.call(signal, 'abort', onAbort, {once: true});
530+
// We need to remove the `abort` listener when the event listener is going to be removed,
531+
// as it creates a closure that captures `task`. This closure retains a reference to the
532+
// `task` object even after it goes out of scope, preventing `task` from being garbage
533+
// collected.
534+
if (data) {
535+
(data as any).removeAbortListener = () => signal.removeEventListener('abort', onAbort);
536+
}
539537
}
540538

541539
// should clear taskData.target to avoid memory leak
@@ -620,7 +618,7 @@ export function patchEventTarget(
620618
if (symbolEventNames) {
621619
symbolEventName = symbolEventNames[capture ? TRUE_STR : FALSE_STR];
622620
}
623-
const existingTasks = symbolEventName && target[symbolEventName];
621+
const existingTasks: Task[] = symbolEventName && target[symbolEventName];
624622
if (existingTasks) {
625623
for (let i = 0; i < existingTasks.length; i++) {
626624
const existingTask = existingTasks[i];
@@ -643,6 +641,15 @@ export function patchEventTarget(
643641
target[onPropertySymbol] = null;
644642
}
645643
}
644+
645+
// Note that `removeAllListeners` would ultimately call `removeEventListener`,
646+
// so we're safe to remove the abort listener only once here.
647+
const taskData = existingTask.data as any;
648+
if (taskData?.removeAbortListener) {
649+
taskData.removeAbortListener();
650+
taskData.removeAbortListener = null;
651+
}
652+
646653
existingTask.zone.cancelTask(existingTask);
647654
if (returnTarget) {
648655
return target;

0 commit comments

Comments
 (0)