Skip to content

Commit fe6b39d

Browse files
matskoalxhub
authored andcommitted
perf(animations): do not create a closure each time a node is removed
1 parent d837bfc commit fe6b39d

1 file changed

Lines changed: 101 additions & 52 deletions

File tree

‎packages/animations/browser/src/render/transition_animation_engine.ts‎

Lines changed: 101 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,24 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru
1313
import {AnimationTrigger} from '../dsl/animation_trigger';
1414
import {ElementInstructionMap} from '../dsl/element_instruction_map';
1515
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
16-
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, LEAVE_SELECTOR, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
16+
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
1717

1818
import {AnimationDriver} from './animation_driver';
1919
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
2020

2121
const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = [];
22-
const NOOP_FN = () => {};
22+
const NULL_REMOVAL_STATE: ElementAnimationState = {
23+
namespaceId: '',
24+
setForRemoval: null,
25+
hasAnimation: false,
26+
removedBeforeQueried: false
27+
};
28+
const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = {
29+
namespaceId: '',
30+
setForRemoval: null,
31+
hasAnimation: false,
32+
removedBeforeQueried: true
33+
};
2334

2435
interface TriggerListener {
2536
name: string;
@@ -37,6 +48,15 @@ export interface QueueInstruction {
3748
isFallbackTransition: boolean;
3849
}
3950

51+
export const REMOVAL_FLAG = '__ng_removed';
52+
53+
export interface ElementAnimationState {
54+
setForRemoval: any;
55+
hasAnimation: boolean;
56+
namespaceId: string;
57+
removedBeforeQueried: boolean;
58+
}
59+
4060
export class StateValue {
4161
public value: string;
4262
public options: AnimationOptions;
@@ -245,7 +265,7 @@ export class AnimationTransitionNamespace {
245265
});
246266
}
247267

248-
private _onElementDestroy(element: any) {
268+
clearElementCache(element: any) {
249269
this._engine.statesByElement.delete(element);
250270
this._elementListeners.delete(element);
251271
const elementPlayers = this._engine.playersByElement.get(element);
@@ -267,14 +287,13 @@ export class AnimationTransitionNamespace {
267287

268288
this.removeNode(elm, context, true);
269289
} else {
270-
this._onElementDestroy(elm);
290+
this.clearElementCache(elm);
271291
}
272292
});
273293
}
274294

275295
removeNode(element: any, context: any, doNotRecurse?: boolean): void {
276296
const engine = this._engine;
277-
engine.markElementAsRemoved(element);
278297

279298
if (!doNotRecurse && element.childElementCount) {
280299
this._destroyInnerNodes(element, context, true);
@@ -295,12 +314,8 @@ export class AnimationTransitionNamespace {
295314
});
296315

297316
if (players.length) {
298-
optimizeGroupPlayer(players).onDone(() => {
299-
engine.destroyInnerAnimations(element);
300-
this._onElementDestroy(element);
301-
engine._onRemovalComplete(element, context);
302-
});
303-
317+
engine.markElementAsRemoved(this.id, element, true, context);
318+
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
304319
return;
305320
}
306321
}
@@ -365,15 +380,11 @@ export class AnimationTransitionNamespace {
365380
// whether or not a parent has an animation we need to delay the deferral of the leave
366381
// operation until we have more information (which we do after flush() has been called)
367382
if (containsPotentialParentTransition) {
368-
engine.queuedRemovals.set(element, () => {
369-
engine.destroyInnerAnimations(element);
370-
this._onElementDestroy(element);
371-
engine._onRemovalComplete(element, context);
372-
});
383+
engine.markElementAsRemoved(this.id, element, false, context);
373384
} else {
374385
// we do this after the flush has occurred such
375386
// that the callbacks can be fired
376-
engine.afterFlush(() => this._onElementDestroy(element));
387+
engine.afterFlush(() => this.clearElementCache(element));
377388
engine.destroyInnerAnimations(element);
378389
engine._onRemovalComplete(element, context);
379390
}
@@ -447,7 +458,6 @@ export interface QueuedTransition {
447458

448459
export class TransitionAnimationEngine {
449460
public players: TransitionAnimationPlayer[] = [];
450-
public queuedRemovals = new Map<any, () => any>();
451461
public newHostElements = new Map<any, AnimationTransitionNamespace>();
452462
public playersByElement = new Map<any, TransitionAnimationPlayer[]>();
453463
public playersByQueriedElement = new Map<any, TransitionAnimationPlayer[]>();
@@ -462,6 +472,7 @@ export class TransitionAnimationEngine {
462472

463473
public namespacesByHostElement = new Map<any, AnimationTransitionNamespace>();
464474
public collectedEnterElements: any[] = [];
475+
public collectedLeaveElements: any[] = [];
465476

466477
// this method is designed to be overridden by the code that uses this engine
467478
public onRemovalComplete = (element: any, context: any) => {};
@@ -572,8 +583,9 @@ export class TransitionAnimationEngine {
572583

573584
// special case for when an element is removed and reinserted (move operation)
574585
// when this occurs we do not want to use the element for deletion later
575-
if (this.queuedRemovals.has(element)) {
576-
this.queuedRemovals.delete(element);
586+
const details = element[REMOVAL_FLAG] as ElementAnimationState;
587+
if (details && details.setForRemoval) {
588+
details.setForRemoval = false;
577589
}
578590

579591
// in the event that the namespaceId is blank then the caller
@@ -592,19 +604,27 @@ export class TransitionAnimationEngine {
592604
collectEnterElement(element: any) { this.collectedEnterElements.push(element); }
593605

594606
removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
595-
if (namespaceId) {
596-
const ns = this._fetchNamespace(namespaceId);
597-
if (!isElementNode(element) || !ns) {
598-
this._onRemovalComplete(element, context);
599-
} else {
600-
ns.removeNode(element, context, doNotRecurse);
601-
}
607+
if (!isElementNode(element)) {
608+
this._onRemovalComplete(element, context);
609+
return;
610+
}
611+
612+
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
613+
if (ns) {
614+
ns.removeNode(element, context, doNotRecurse);
602615
} else {
603-
this.queuedRemovals.set(element, () => this._onRemovalComplete(element, context));
616+
this.markElementAsRemoved(namespaceId, element, false, context);
604617
}
605618
}
606619

607-
markElementAsRemoved(element: any) { this.queuedRemovals.set(element, NOOP_FN); }
620+
markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) {
621+
this.collectedLeaveElements.push(element);
622+
element[REMOVAL_FLAG] = {
623+
namespaceId,
624+
setForRemoval: context, hasAnimation,
625+
removedBeforeQueried: false
626+
};
627+
}
608628

609629
listen(
610630
namespaceId: string, element: any, name: string, phase: string,
@@ -653,22 +673,42 @@ export class TransitionAnimationEngine {
653673
});
654674
}
655675

676+
processLeaveNode(element: any) {
677+
const details = element[REMOVAL_FLAG] as ElementAnimationState;
678+
if (details && details.setForRemoval) {
679+
// this will prevent it from removing it twice
680+
element[REMOVAL_FLAG] = NULL_REMOVAL_STATE;
681+
if (details.namespaceId) {
682+
this.destroyInnerAnimations(element);
683+
const ns = this._fetchNamespace(details.namespaceId);
684+
if (ns) {
685+
ns.clearElementCache(element);
686+
}
687+
}
688+
this._onRemovalComplete(element, details.setForRemoval);
689+
}
690+
}
691+
656692
flush(microtaskId: number = -1) {
657693
let players: AnimationPlayer[] = [];
658694
if (this.newHostElements.size) {
659695
this.newHostElements.forEach((ns, element) => this._balanceNamespaceList(ns, element));
660696
this.newHostElements.clear();
661697
}
662698

663-
if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) {
699+
if (this._namespaceList.length &&
700+
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
664701
players = this._flushAnimations(microtaskId);
665702
} else {
666-
this.queuedRemovals.forEach(fn => fn());
703+
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
704+
const element = this.collectedLeaveElements[i];
705+
this.processLeaveNode(element);
706+
}
667707
}
668708

669709
this.totalQueuedPlayers = 0;
670710
this.collectedEnterElements.length = 0;
671-
this.queuedRemovals.clear();
711+
this.collectedLeaveElements.length = 0;
672712
this._flushFns.forEach(fn => fn());
673713
this._flushFns = [];
674714

@@ -704,7 +744,17 @@ export class TransitionAnimationEngine {
704744
const enterNodes: any[] =
705745
allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : [];
706746

707-
this.queuedRemovals.forEach((fn, element) => addClass(element, LEAVE_CLASSNAME));
747+
const leaveNodes: any[] = [];
748+
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
749+
const element = this.collectedLeaveElements[i];
750+
if (isElementNode(element)) {
751+
const details = element[REMOVAL_FLAG] as ElementAnimationState;
752+
if (details && details.setForRemoval) {
753+
addClass(element, LEAVE_CLASSNAME);
754+
leaveNodes.push(element);
755+
}
756+
}
757+
}
708758

709759
for (let i = this._namespaceList.length - 1; i >= 0; i--) {
710760
const ns = this._namespaceList[i];
@@ -788,10 +838,6 @@ export class TransitionAnimationEngine {
788838

789839
allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
790840

791-
const leaveNodes: any[] = bodyNode && allPostStyleElements.size ?
792-
this.driver.query(bodyNode, LEAVE_SELECTOR, true) :
793-
[];
794-
795841
// PRE STAGE: fill the ! styles
796842
const preStylesMap = allPreStyleElements.size ?
797843
cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) :
@@ -861,14 +907,18 @@ export class TransitionAnimationEngine {
861907
// run through all of the queued removals and see if they
862908
// were picked up by a query. If not then perform the removal
863909
// operation right away unless a parent animation is ongoing.
864-
this.queuedRemovals.forEach((fn, element) => {
910+
for (let i = 0; i < leaveNodes.length; i++) {
911+
const element = leaveNodes[i];
865912
const players = queriedElements.get(element);
866913
if (players) {
867-
optimizeGroupPlayer(players).onDone(fn);
914+
removeNodesAfterAnimationDone(this, element, players);
868915
} else {
869-
fn();
916+
const details = element[REMOVAL_FLAG] as ElementAnimationState;
917+
if (details && !details.hasAnimation) {
918+
this.processLeaveNode(element);
919+
}
870920
}
871-
});
921+
}
872922

873923
rootPlayers.forEach(player => {
874924
this.players.push(player);
@@ -888,7 +938,8 @@ export class TransitionAnimationEngine {
888938

889939
elementContainsData(namespaceId: string, element: any) {
890940
let containsData = false;
891-
if (this.queuedRemovals.has(element)) containsData = true;
941+
const details = element[REMOVAL_FLAG] as ElementAnimationState;
942+
if (details && details.setForRemoval) containsData = true;
892943
if (this.playersByElement.has(element)) containsData = true;
893944
if (this.playersByQueriedElement.has(element)) containsData = true;
894945
if (this.statesByElement.has(element)) containsData = true;
@@ -979,7 +1030,8 @@ export class TransitionAnimationEngine {
9791030
const element = timelineInstruction.element;
9801031

9811032
// FIXME (matsko): make sure to-be-removed animations are removed properly
982-
if (element['REMOVED']) return new NoopAnimationPlayer();
1033+
const details = element[REMOVAL_FLAG];
1034+
if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
9831035

9841036
const isQueriedElement = element !== rootElement;
9851037
let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY;
@@ -1229,7 +1281,7 @@ function cloakAndComputeStyles(
12291281
// there is no easy way to detect this because a sub element could be removed
12301282
// by a parent animation element being detached.
12311283
if (!value || value.length == 0) {
1232-
element['REMOVED'] = true;
1284+
element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE;
12331285
}
12341286
});
12351287
valuesMap.set(element, styles);
@@ -1280,17 +1332,14 @@ function removeClass(element: any, className: string) {
12801332
}
12811333
}
12821334

1283-
function setAttribute(element: any, attr: string, value: any) {
1284-
if (element.setAttribute) {
1285-
element.setAttribute(attr, value);
1286-
} else {
1287-
element[attr] = value;
1288-
}
1289-
}
1290-
12911335
function getBodyNode(): any|null {
12921336
if (typeof document != 'undefined') {
12931337
return document.body;
12941338
}
12951339
return null;
12961340
}
1341+
1342+
function removeNodesAfterAnimationDone(
1343+
engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
1344+
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
1345+
}

0 commit comments

Comments
 (0)