fix(core): avoid leaking view data in animations

The animations code currently tracks which views have running leave animations by adding them to a `Set`. This can leak memory if we don't clean something up on time.

These changes switch to tracking the views by their ID which doesn't risk retaining the view.

Fixes #66255.
This commit is contained in:
Kristiyan Kostadinov
2025-12-26 10:14:21 +02:00
committed by Kirill Cherkashin
parent a0dfa5fa86
commit 80b0fbba1f
4 changed files with 12 additions and 9 deletions
@@ -147,4 +147,5 @@ function determineLongestAnimationFromElementAnimations(
}
}
export const allLeavingAnimations = new Set<LView>();
/** Tracks the IDs of LViews with leaving animations. */
export const allLeavingAnimations = new Set<number>();
@@ -13,7 +13,7 @@ import {
MAX_ANIMATION_TIMEOUT,
} from '../../animation/interfaces';
import {getLView, getCurrentTNode} from '../state';
import {RENDERER, INJECTOR, CONTEXT, LView} from '../interfaces/view';
import {RENDERER, INJECTOR, CONTEXT, LView, ID} from '../interfaces/view';
import {getNativeByTNode} from '../util/view_utils';
import {performanceMarkFeature} from '../../util/performance';
import {Renderer} from '../interfaces/renderer';
@@ -277,7 +277,7 @@ function runLeaveAnimations(
const renderer = lView[RENDERER];
const ngZone = lView[INJECTOR].get(NgZone);
allLeavingAnimations.add(lView);
allLeavingAnimations.add(lView[ID]);
(getLViewLeaveAnimations(lView).get(tNode.index)!.resolvers ??= []).push(resolve);
const activeClasses = getClassListFromValue(value);
@@ -389,7 +389,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa
const tNode = getCurrentTNode()!;
cancelLeavingNodes(tNode, lView);
allLeavingAnimations.add(lView);
allLeavingAnimations.add(lView[ID]);
addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () =>
runLeaveAnimationFunction(lView, tNode, value),
@@ -28,6 +28,7 @@ import {
DECLARATION_COMPONENT_VIEW,
HEADER_OFFSET,
HYDRATION,
ID,
INJECTOR,
LView,
TVIEW,
@@ -578,7 +579,7 @@ function clearDetachAnimationList(lContainer: LContainer, index: number): void {
if (lContainer.length <= CONTAINER_HEADER_OFFSET) return;
const indexInContainer = CONTAINER_HEADER_OFFSET + index;
const viewToDetach = lContainer[indexInContainer];
const viewToDetach = lContainer[indexInContainer] as LView;
const animations = viewToDetach
? (viewToDetach[ANIMATIONS] as AnimationLViewData | undefined)
: undefined;
@@ -590,7 +591,7 @@ function clearDetachAnimationList(lContainer: LContainer, index: number): void {
) {
const injector = viewToDetach[INJECTOR];
removeFromAnimationQueue(injector, animations);
allLeavingAnimations.delete(viewToDetach);
allLeavingAnimations.delete(viewToDetach[ID]);
animations.detachedLeaveAnimationFns = undefined;
}
}
@@ -76,6 +76,7 @@ import {
TView,
TViewType,
INJECTOR,
ID,
} from './interfaces/view';
import {assertTNodeType} from './node_assert';
import {profiler} from './profiler';
@@ -387,7 +388,7 @@ function runLeaveAnimationsWithCallback(
if (animations == null || animations.leave == undefined || !animations.leave.has(tNode.index))
return callback(false);
if (lView) allLeavingAnimations.add(lView);
if (lView) allLeavingAnimations.add(lView[ID]);
addToAnimationQueue(
injector,
@@ -410,7 +411,7 @@ function runLeaveAnimationsWithCallback(
animations.running = Promise.allSettled(runningAnimations);
runAfterLeaveAnimations(lView!, callback);
} else {
if (lView) allLeavingAnimations.delete(lView);
if (lView) allLeavingAnimations.delete(lView[ID]);
callback(false);
}
},
@@ -423,7 +424,7 @@ function runAfterLeaveAnimations(lView: LView, callback: Function) {
if (runningAnimations) {
runningAnimations.then(() => {
lView[ANIMATIONS]!.running = undefined;
allLeavingAnimations.delete(lView);
allLeavingAnimations.delete(lView[ID]);
callback(true);
});
return;