perf(animations): do not create a closure each time a node is removed

This commit is contained in:
Matias Niemelä 2017-06-06 13:17:45 -07:00 committed by Alex Rickabaugh
parent d837bfc2d7
commit fe6b39d585

View File

@ -13,13 +13,24 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru
import {AnimationTrigger} from '../dsl/animation_trigger'; import {AnimationTrigger} from '../dsl/animation_trigger';
import {ElementInstructionMap} from '../dsl/element_instruction_map'; import {ElementInstructionMap} from '../dsl/element_instruction_map';
import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer';
import {ENTER_CLASSNAME, LEAVE_CLASSNAME, LEAVE_SELECTOR, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util'; import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util';
import {AnimationDriver} from './animation_driver'; import {AnimationDriver} from './animation_driver';
import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared'; import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared';
const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = []; const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = [];
const NOOP_FN = () => {}; const NULL_REMOVAL_STATE: ElementAnimationState = {
namespaceId: '',
setForRemoval: null,
hasAnimation: false,
removedBeforeQueried: false
};
const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = {
namespaceId: '',
setForRemoval: null,
hasAnimation: false,
removedBeforeQueried: true
};
interface TriggerListener { interface TriggerListener {
name: string; name: string;
@ -37,6 +48,15 @@ export interface QueueInstruction {
isFallbackTransition: boolean; isFallbackTransition: boolean;
} }
export const REMOVAL_FLAG = '__ng_removed';
export interface ElementAnimationState {
setForRemoval: any;
hasAnimation: boolean;
namespaceId: string;
removedBeforeQueried: boolean;
}
export class StateValue { export class StateValue {
public value: string; public value: string;
public options: AnimationOptions; public options: AnimationOptions;
@ -245,7 +265,7 @@ export class AnimationTransitionNamespace {
}); });
} }
private _onElementDestroy(element: any) { clearElementCache(element: any) {
this._engine.statesByElement.delete(element); this._engine.statesByElement.delete(element);
this._elementListeners.delete(element); this._elementListeners.delete(element);
const elementPlayers = this._engine.playersByElement.get(element); const elementPlayers = this._engine.playersByElement.get(element);
@ -267,14 +287,13 @@ export class AnimationTransitionNamespace {
this.removeNode(elm, context, true); this.removeNode(elm, context, true);
} else { } else {
this._onElementDestroy(elm); this.clearElementCache(elm);
} }
}); });
} }
removeNode(element: any, context: any, doNotRecurse?: boolean): void { removeNode(element: any, context: any, doNotRecurse?: boolean): void {
const engine = this._engine; const engine = this._engine;
engine.markElementAsRemoved(element);
if (!doNotRecurse && element.childElementCount) { if (!doNotRecurse && element.childElementCount) {
this._destroyInnerNodes(element, context, true); this._destroyInnerNodes(element, context, true);
@ -295,12 +314,8 @@ export class AnimationTransitionNamespace {
}); });
if (players.length) { if (players.length) {
optimizeGroupPlayer(players).onDone(() => { engine.markElementAsRemoved(this.id, element, true, context);
engine.destroyInnerAnimations(element); optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
this._onElementDestroy(element);
engine._onRemovalComplete(element, context);
});
return; return;
} }
} }
@ -365,15 +380,11 @@ export class AnimationTransitionNamespace {
// whether or not a parent has an animation we need to delay the deferral of the leave // whether or not a parent has an animation we need to delay the deferral of the leave
// operation until we have more information (which we do after flush() has been called) // operation until we have more information (which we do after flush() has been called)
if (containsPotentialParentTransition) { if (containsPotentialParentTransition) {
engine.queuedRemovals.set(element, () => { engine.markElementAsRemoved(this.id, element, false, context);
engine.destroyInnerAnimations(element);
this._onElementDestroy(element);
engine._onRemovalComplete(element, context);
});
} else { } else {
// we do this after the flush has occurred such // we do this after the flush has occurred such
// that the callbacks can be fired // that the callbacks can be fired
engine.afterFlush(() => this._onElementDestroy(element)); engine.afterFlush(() => this.clearElementCache(element));
engine.destroyInnerAnimations(element); engine.destroyInnerAnimations(element);
engine._onRemovalComplete(element, context); engine._onRemovalComplete(element, context);
} }
@ -447,7 +458,6 @@ export interface QueuedTransition {
export class TransitionAnimationEngine { export class TransitionAnimationEngine {
public players: TransitionAnimationPlayer[] = []; public players: TransitionAnimationPlayer[] = [];
public queuedRemovals = new Map<any, () => any>();
public newHostElements = new Map<any, AnimationTransitionNamespace>(); public newHostElements = new Map<any, AnimationTransitionNamespace>();
public playersByElement = new Map<any, TransitionAnimationPlayer[]>(); public playersByElement = new Map<any, TransitionAnimationPlayer[]>();
public playersByQueriedElement = new Map<any, TransitionAnimationPlayer[]>(); public playersByQueriedElement = new Map<any, TransitionAnimationPlayer[]>();
@ -462,6 +472,7 @@ export class TransitionAnimationEngine {
public namespacesByHostElement = new Map<any, AnimationTransitionNamespace>(); public namespacesByHostElement = new Map<any, AnimationTransitionNamespace>();
public collectedEnterElements: any[] = []; public collectedEnterElements: any[] = [];
public collectedLeaveElements: any[] = [];
// this method is designed to be overridden by the code that uses this engine // this method is designed to be overridden by the code that uses this engine
public onRemovalComplete = (element: any, context: any) => {}; public onRemovalComplete = (element: any, context: any) => {};
@ -572,8 +583,9 @@ export class TransitionAnimationEngine {
// special case for when an element is removed and reinserted (move operation) // special case for when an element is removed and reinserted (move operation)
// when this occurs we do not want to use the element for deletion later // when this occurs we do not want to use the element for deletion later
if (this.queuedRemovals.has(element)) { const details = element[REMOVAL_FLAG] as ElementAnimationState;
this.queuedRemovals.delete(element); if (details && details.setForRemoval) {
details.setForRemoval = false;
} }
// in the event that the namespaceId is blank then the caller // in the event that the namespaceId is blank then the caller
@ -592,19 +604,27 @@ export class TransitionAnimationEngine {
collectEnterElement(element: any) { this.collectedEnterElements.push(element); } collectEnterElement(element: any) { this.collectedEnterElements.push(element); }
removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void { removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
if (namespaceId) { if (!isElementNode(element)) {
const ns = this._fetchNamespace(namespaceId); this._onRemovalComplete(element, context);
if (!isElementNode(element) || !ns) { return;
this._onRemovalComplete(element, context); }
} else {
ns.removeNode(element, context, doNotRecurse); const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
} if (ns) {
ns.removeNode(element, context, doNotRecurse);
} else { } else {
this.queuedRemovals.set(element, () => this._onRemovalComplete(element, context)); this.markElementAsRemoved(namespaceId, element, false, context);
} }
} }
markElementAsRemoved(element: any) { this.queuedRemovals.set(element, NOOP_FN); } markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) {
this.collectedLeaveElements.push(element);
element[REMOVAL_FLAG] = {
namespaceId,
setForRemoval: context, hasAnimation,
removedBeforeQueried: false
};
}
listen( listen(
namespaceId: string, element: any, name: string, phase: string, namespaceId: string, element: any, name: string, phase: string,
@ -653,6 +673,22 @@ export class TransitionAnimationEngine {
}); });
} }
processLeaveNode(element: any) {
const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) {
// this will prevent it from removing it twice
element[REMOVAL_FLAG] = NULL_REMOVAL_STATE;
if (details.namespaceId) {
this.destroyInnerAnimations(element);
const ns = this._fetchNamespace(details.namespaceId);
if (ns) {
ns.clearElementCache(element);
}
}
this._onRemovalComplete(element, details.setForRemoval);
}
}
flush(microtaskId: number = -1) { flush(microtaskId: number = -1) {
let players: AnimationPlayer[] = []; let players: AnimationPlayer[] = [];
if (this.newHostElements.size) { if (this.newHostElements.size) {
@ -660,15 +696,19 @@ export class TransitionAnimationEngine {
this.newHostElements.clear(); this.newHostElements.clear();
} }
if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) { if (this._namespaceList.length &&
(this.totalQueuedPlayers || this.collectedLeaveElements.length)) {
players = this._flushAnimations(microtaskId); players = this._flushAnimations(microtaskId);
} else { } else {
this.queuedRemovals.forEach(fn => fn()); for (let i = 0; i < this.collectedLeaveElements.length; i++) {
const element = this.collectedLeaveElements[i];
this.processLeaveNode(element);
}
} }
this.totalQueuedPlayers = 0; this.totalQueuedPlayers = 0;
this.collectedEnterElements.length = 0; this.collectedEnterElements.length = 0;
this.queuedRemovals.clear(); this.collectedLeaveElements.length = 0;
this._flushFns.forEach(fn => fn()); this._flushFns.forEach(fn => fn());
this._flushFns = []; this._flushFns = [];
@ -704,7 +744,17 @@ export class TransitionAnimationEngine {
const enterNodes: any[] = const enterNodes: any[] =
allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : []; allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : [];
this.queuedRemovals.forEach((fn, element) => addClass(element, LEAVE_CLASSNAME)); const leaveNodes: any[] = [];
for (let i = 0; i < this.collectedLeaveElements.length; i++) {
const element = this.collectedLeaveElements[i];
if (isElementNode(element)) {
const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) {
addClass(element, LEAVE_CLASSNAME);
leaveNodes.push(element);
}
}
}
for (let i = this._namespaceList.length - 1; i >= 0; i--) { for (let i = this._namespaceList.length - 1; i >= 0; i--) {
const ns = this._namespaceList[i]; const ns = this._namespaceList[i];
@ -788,10 +838,6 @@ export class TransitionAnimationEngine {
allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy())); allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy()));
const leaveNodes: any[] = bodyNode && allPostStyleElements.size ?
this.driver.query(bodyNode, LEAVE_SELECTOR, true) :
[];
// PRE STAGE: fill the ! styles // PRE STAGE: fill the ! styles
const preStylesMap = allPreStyleElements.size ? const preStylesMap = allPreStyleElements.size ?
cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) : cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) :
@ -861,14 +907,18 @@ export class TransitionAnimationEngine {
// run through all of the queued removals and see if they // run through all of the queued removals and see if they
// were picked up by a query. If not then perform the removal // were picked up by a query. If not then perform the removal
// operation right away unless a parent animation is ongoing. // operation right away unless a parent animation is ongoing.
this.queuedRemovals.forEach((fn, element) => { for (let i = 0; i < leaveNodes.length; i++) {
const element = leaveNodes[i];
const players = queriedElements.get(element); const players = queriedElements.get(element);
if (players) { if (players) {
optimizeGroupPlayer(players).onDone(fn); removeNodesAfterAnimationDone(this, element, players);
} else { } else {
fn(); const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && !details.hasAnimation) {
this.processLeaveNode(element);
}
} }
}); }
rootPlayers.forEach(player => { rootPlayers.forEach(player => {
this.players.push(player); this.players.push(player);
@ -888,7 +938,8 @@ export class TransitionAnimationEngine {
elementContainsData(namespaceId: string, element: any) { elementContainsData(namespaceId: string, element: any) {
let containsData = false; let containsData = false;
if (this.queuedRemovals.has(element)) containsData = true; const details = element[REMOVAL_FLAG] as ElementAnimationState;
if (details && details.setForRemoval) containsData = true;
if (this.playersByElement.has(element)) containsData = true; if (this.playersByElement.has(element)) containsData = true;
if (this.playersByQueriedElement.has(element)) containsData = true; if (this.playersByQueriedElement.has(element)) containsData = true;
if (this.statesByElement.has(element)) containsData = true; if (this.statesByElement.has(element)) containsData = true;
@ -979,7 +1030,8 @@ export class TransitionAnimationEngine {
const element = timelineInstruction.element; const element = timelineInstruction.element;
// FIXME (matsko): make sure to-be-removed animations are removed properly // FIXME (matsko): make sure to-be-removed animations are removed properly
if (element['REMOVED']) return new NoopAnimationPlayer(); const details = element[REMOVAL_FLAG];
if (details && details.removedBeforeQueried) return new NoopAnimationPlayer();
const isQueriedElement = element !== rootElement; const isQueriedElement = element !== rootElement;
let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY; let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY;
@ -1229,7 +1281,7 @@ function cloakAndComputeStyles(
// there is no easy way to detect this because a sub element could be removed // there is no easy way to detect this because a sub element could be removed
// by a parent animation element being detached. // by a parent animation element being detached.
if (!value || value.length == 0) { if (!value || value.length == 0) {
element['REMOVED'] = true; element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE;
} }
}); });
valuesMap.set(element, styles); valuesMap.set(element, styles);
@ -1280,17 +1332,14 @@ function removeClass(element: any, className: string) {
} }
} }
function setAttribute(element: any, attr: string, value: any) {
if (element.setAttribute) {
element.setAttribute(attr, value);
} else {
element[attr] = value;
}
}
function getBodyNode(): any|null { function getBodyNode(): any|null {
if (typeof document != 'undefined') { if (typeof document != 'undefined') {
return document.body; return document.body;
} }
return null; return null;
} }
function removeNodesAfterAnimationDone(
engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) {
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
}