Skip to content

Commit 18c9352

Browse files
thePunderWomanAndrewKushnir
authored andcommitted
fix(core): fix memory leak with leaving nodes tracking (#63328)
This ensures the nodes are properly cleaned up from the leaving nodes map. PR Close #63328
1 parent d16a992 commit 18c9352

File tree

1 file changed

+30
-18
lines changed

1 file changed

+30
-18
lines changed

packages/core/src/render3/instructions/animation.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,30 @@ const longestAnimations = new WeakMap<HTMLElement, LongestAnimation>();
8888
// from an `@if` or `@for`.
8989
const leavingNodes = new WeakMap<TNode, HTMLElement[]>();
9090

91-
function clearLeavingNodes(tNode: TNode): void {
92-
if (leavingNodes.get(tNode)?.length === 0) {
91+
function clearLeavingNodes(tNode: TNode, el: HTMLElement): void {
92+
const nodes = leavingNodes.get(tNode);
93+
if (nodes && nodes.length > 0) {
94+
const ix = nodes.findIndex((node) => node === el);
95+
if (ix > -1) nodes.splice(ix, 1);
96+
}
97+
if (nodes?.length === 0) {
9398
leavingNodes.delete(tNode);
9499
}
95100
}
96101

102+
/**
103+
* In the case that we have an existing node that's animating away, like when
104+
* an `@if` toggles quickly or `@for` adds and removes elements quickly, we
105+
* need to end the animation for the former node and remove it right away to
106+
* prevent duplicate nodes showing up.
107+
*/
108+
function cancelLeavingNodes(tNode: TNode, el: HTMLElement): void {
109+
leavingNodes
110+
.get(tNode)
111+
?.pop()
112+
?.dispatchEvent(new CustomEvent('animationend', {detail: {cancel: true}}));
113+
}
114+
97115
function trackLeavingNodes(tNode: TNode, el: HTMLElement): void {
98116
// We need to track this tNode's element just to be sure we don't add
99117
// a new RNode for this TNode while this one is still animating away.
@@ -160,15 +178,6 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
160178
cleanupFns.push(renderer.listen(nativeElement, 'transitionstart', handleAnimationStart));
161179
});
162180

163-
// In the case that we have an existing node that's animating away, like when
164-
// an `@if` toggles quickly or `@for` adds and removes elements quickly, we
165-
// need to end the animation for the former node and remove it right away to
166-
// prevent duplicate nodes showing up.
167-
leavingNodes
168-
.get(tNode)
169-
?.pop()
170-
?.dispatchEvent(new CustomEvent('animationend', {detail: {cancel: true}}));
171-
172181
trackEnterClasses(nativeElement, activeClasses, cleanupFns);
173182

174183
for (const klass of activeClasses) {
@@ -179,6 +188,12 @@ export function ɵɵanimateEnter(value: string | Function): typeof ɵɵanimateEn
179188
// preventing an animation via selector specificity.
180189
ngZone.runOutsideAngular(() => {
181190
requestAnimationFrame(() => {
191+
// In the case that we have an existing node that's animating away, like when
192+
// an `@if` toggles quickly or `@for` adds and removes elements quickly, we
193+
// need to end the animation for the former node and remove it right away to
194+
// prevent duplicate nodes showing up.
195+
cancelLeavingNodes(tNode, nativeElement);
196+
182197
determineLongestAnimation(nativeElement, longestAnimations, areAnimationSupported);
183198
if (!longestAnimations.has(nativeElement)) {
184199
for (const klass of activeClasses) {
@@ -242,10 +257,7 @@ export function ɵɵanimateEnterListener(value: AnimationFunction): typeof ɵɵa
242257
// an `@if` toggles quickly or `@for` adds and removes elements quickly, we
243258
// need to end the animation for the former node and remove it right away to
244259
// prevent duplicate nodes showing up.
245-
leavingNodes
246-
.get(tNode)
247-
?.pop()
248-
?.dispatchEvent(new CustomEvent('animationend', {detail: {cancel: true}}));
260+
cancelLeavingNodes(tNode, nativeElement);
249261

250262
value.call(lView[CONTEXT], {target: nativeElement, animationComplete: noOpAnimationComplete});
251263

@@ -375,7 +387,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
375387
const event: AnimationCallbackEvent = {
376388
target: nativeElement,
377389
animationComplete: () => {
378-
clearLeavingNodes(tNode);
390+
clearLeavingNodes(tNode, _el as HTMLElement);
379391
removeFn();
380392
},
381393
};
@@ -530,7 +542,7 @@ function animateLeaveClassRunner(
530542
// affect any other animations on the page.
531543
event.stopImmediatePropagation();
532544
longestAnimations.delete(el);
533-
clearLeavingNodes(tNode);
545+
clearLeavingNodes(tNode, el);
534546
finalRemoveFn();
535547
}
536548
};
@@ -550,7 +562,7 @@ function animateLeaveClassRunner(
550562
requestAnimationFrame(() => {
551563
determineLongestAnimation(el, longestAnimations, areAnimationSupported);
552564
if (!longestAnimations.has(el)) {
553-
clearLeavingNodes(tNode);
565+
clearLeavingNodes(tNode, el);
554566
finalRemoveFn();
555567
}
556568
});

0 commit comments

Comments
 (0)