Skip to content

Commit e2622ad

Browse files
matskovicb
authored andcommitted
perf(animations): always run the animation queue outside of zones
Related #12732 Closes #13440
1 parent ecfad46 commit e2622ad

File tree

17 files changed

+439
-104
lines changed

17 files changed

+439
-104
lines changed

modules/@angular/compiler/src/compiler_util/render_util.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ function sanitizedValue(
9191

9292
export function triggerAnimation(
9393
view: o.Expression, componentView: o.Expression, boundProp: BoundElementPropertyAst,
94-
eventListener: o.Expression, renderElement: o.Expression, renderValue: o.Expression,
95-
lastRenderValue: o.Expression) {
94+
boundOutputs: BoundEventAst[], eventListener: o.Expression, renderElement: o.Expression,
95+
renderValue: o.Expression, lastRenderValue: o.Expression) {
9696
const detachStmts: o.Statement[] = [];
9797
const updateStmts: o.Statement[] = [];
9898

@@ -121,23 +121,32 @@ export function triggerAnimation(
121121
.set(animationFnExpr.callFn([view, renderElement, lastRenderValue, emptyStateValue]))
122122
.toDeclStmt());
123123

124-
const registerStmts = [
125-
animationTransitionVar
126-
.callMethod(
127-
'onStart',
128-
[eventListener.callMethod(
129-
o.BuiltinMethod.Bind,
130-
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
131-
.toStmt(),
132-
animationTransitionVar
133-
.callMethod(
134-
'onDone',
135-
[eventListener.callMethod(
136-
o.BuiltinMethod.Bind,
137-
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
138-
.toStmt(),
124+
const registerStmts: o.Statement[] = [];
125+
const animationStartMethodExists = boundOutputs.find(
126+
event => event.isAnimation && event.name == animationName && event.phase == 'start');
127+
if (animationStartMethodExists) {
128+
registerStmts.push(
129+
animationTransitionVar
130+
.callMethod(
131+
'onStart',
132+
[eventListener.callMethod(
133+
o.BuiltinMethod.Bind,
134+
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
135+
.toStmt());
136+
}
139137

140-
];
138+
const animationDoneMethodExists = boundOutputs.find(
139+
event => event.isAnimation && event.name == animationName && event.phase == 'done');
140+
if (animationDoneMethodExists) {
141+
registerStmts.push(
142+
animationTransitionVar
143+
.callMethod(
144+
'onDone',
145+
[eventListener.callMethod(
146+
o.BuiltinMethod.Bind,
147+
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
148+
.toStmt());
149+
}
141150

142151
updateStmts.push(...registerStmts);
143152
detachStmts.push(...registerStmts);

modules/@angular/compiler/src/directive_wrapper_compiler.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class DirectiveWrapperCompiler {
7070
addCheckInputMethod(inputFieldName, builder);
7171
});
7272
addNgDoCheckMethod(builder);
73-
addCheckHostMethod(hostParseResult.hostProps, builder);
73+
addCheckHostMethod(hostParseResult.hostProps, hostParseResult.hostListeners, builder);
7474
addHandleEventMethod(hostParseResult.hostListeners, builder);
7575
addSubscribeMethod(dirMeta, builder);
7676

@@ -235,7 +235,8 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
235235
}
236236

237237
function addCheckHostMethod(
238-
hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) {
238+
hostProps: BoundElementPropertyAst[], hostEvents: BoundEventAst[],
239+
builder: DirectiveWrapperBuilder) {
239240
const stmts: o.Statement[] = [];
240241
const methodParams: o.FnParam[] = [
241242
new o.FnParam(
@@ -262,7 +263,7 @@ function addCheckHostMethod(
262263
let checkBindingStmts: o.Statement[];
263264
if (hostProp.isAnimation) {
264265
const {updateStmts, detachStmts} = triggerAnimation(
265-
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp,
266+
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp, hostEvents,
266267
o.THIS_EXPR.prop(EVENT_HANDLER_FIELD_NAME)
267268
.or(o.importExpr(createIdentifier(Identifiers.noop))),
268269
RENDER_EL_VAR, evalResult.currValExpr, field.expression);

modules/@angular/compiler/src/view_compiler/property_binder.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {Identifiers, createIdentifier} from '../identifiers';
1717
import * as o from '../output/output_ast';
1818
import {isDefaultChangeDetectionStrategy} from '../private_import_core';
1919
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
20-
import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
20+
import {BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
2121
import {CompileElement, CompileNode} from './compile_element';
2222
import {CompileView} from './compile_view';
2323
import {DetectChangesVars} from './constants';
@@ -41,7 +41,8 @@ export function bindRenderText(
4141
}
4242

4343
export function bindRenderInputs(
44-
boundProps: BoundElementPropertyAst[], hasEvents: boolean, compileElement: CompileElement) {
44+
boundProps: BoundElementPropertyAst[], boundOutputs: BoundEventAst[], hasEvents: boolean,
45+
compileElement: CompileElement) {
4546
const view = compileElement.view;
4647
const renderNode = compileElement.renderNode;
4748

@@ -67,7 +68,7 @@ export function bindRenderInputs(
6768
case PropertyBindingType.Animation:
6869
compileMethod = view.animationBindingsMethod;
6970
const {updateStmts, detachStmts} = triggerAnimation(
70-
o.THIS_EXPR, o.THIS_EXPR, boundProp,
71+
o.THIS_EXPR, o.THIS_EXPR, boundProp, boundOutputs,
7172
(hasEvents ? o.THIS_EXPR.prop(getHandleEventMethodName(compileElement.nodeIndex)) :
7273
o.importExpr(createIdentifier(Identifiers.noop)))
7374
.callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR]),

modules/@angular/compiler/src/view_compiler/view_binder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
4444
visitElement(ast: ElementAst, parent: CompileElement): any {
4545
const compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
4646
const hasEvents = bindOutputs(ast.outputs, ast.directives, compileElement, true);
47-
bindRenderInputs(ast.inputs, hasEvents, compileElement);
47+
bindRenderInputs(ast.inputs, ast.outputs, hasEvents, compileElement);
4848
ast.directives.forEach((directiveAst, dirIndex) => {
4949
const directiveWrapperInstance =
5050
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);

modules/@angular/core/src/animation/animation_queue.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,47 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
8+
import {Injectable} from '../di/metadata';
9+
import {NgZone} from '../zone/ng_zone';
910
import {AnimationPlayer} from './animation_player';
1011

11-
let _queuedAnimations: AnimationPlayer[] = [];
12+
@Injectable()
13+
export class AnimationQueue {
14+
public entries: AnimationPlayer[] = [];
1215

13-
/** @internal */
14-
export function queueAnimation(player: AnimationPlayer) {
15-
_queuedAnimations.push(player);
16-
}
16+
constructor(private _zone: NgZone) {}
17+
18+
enqueue(player: AnimationPlayer) { this.entries.push(player); }
1719

18-
/** @internal */
19-
export function triggerQueuedAnimations() {
20-
// this code is wrapped into a single promise such that the
21-
// onStart and onDone player callbacks are triggered outside
22-
// of the digest cycle of animations
23-
if (_queuedAnimations.length) {
24-
Promise.resolve(null).then(_triggerAnimations);
20+
flush() {
21+
// given that each animation player may set aside
22+
// microtasks and rely on DOM-based events, this
23+
// will cause Angular to run change detection after
24+
// each request. This sidesteps the issue. If a user
25+
// hooks into an animation via (@anim.start) or (@anim.done)
26+
// then those methods will automatically trigger change
27+
// detection by wrapping themselves inside of a zone
28+
if (this.entries.length) {
29+
this._zone.runOutsideAngular(() => {
30+
// this code is wrapped into a single promise such that the
31+
// onStart and onDone player callbacks are triggered outside
32+
// of the digest cycle of animations
33+
Promise.resolve(null).then(() => this._triggerAnimations());
34+
});
35+
}
2536
}
26-
}
2737

28-
function _triggerAnimations() {
29-
for (let i = 0; i < _queuedAnimations.length; i++) {
30-
const player = _queuedAnimations[i];
31-
player.play();
38+
private _triggerAnimations() {
39+
NgZone.assertNotInAngularZone();
40+
41+
while (this.entries.length) {
42+
const player = this.entries.shift();
43+
// in the event that an animation throws an error then we do
44+
// not want to re-run animations on any previous animations
45+
// if they have already been kicked off beforehand
46+
if (!player.hasStarted()) {
47+
player.play();
48+
}
49+
}
3250
}
33-
_queuedAnimations = [];
3451
}

modules/@angular/core/src/animation/animation_transition.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ export class AnimationTransition {
2323
}
2424

2525
onStart(callback: (event: AnimationTransitionEvent) => any): void {
26-
const event = this._createEvent('start');
27-
this._player.onStart(() => callback(event));
26+
const fn =
27+
<() => void>Zone.current.wrap(() => callback(this._createEvent('start')), 'player.onStart');
28+
this._player.onStart(fn);
2829
}
2930

3031
onDone(callback: (event: AnimationTransitionEvent) => any): void {
31-
const event = this._createEvent('done');
32-
this._player.onDone(() => callback(event));
32+
const fn =
33+
<() => void>Zone.current.wrap(() => callback(this._createEvent('done')), 'player.onDone');
34+
this._player.onDone(fn);
3335
}
3436
}

modules/@angular/core/src/application_module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {AnimationQueue} from './animation/animation_queue';
910
import {ApplicationInitStatus} from './application_init';
1011
import {ApplicationRef, ApplicationRef_} from './application_ref';
1112
import {APP_ID_RANDOM_PROVIDER} from './application_tokens';
@@ -37,6 +38,7 @@ export function _keyValueDiffersFactory() {
3738
Compiler,
3839
APP_ID_RANDOM_PROVIDER,
3940
ViewUtils,
41+
AnimationQueue,
4042
{provide: IterableDiffers, useFactory: _iterableDiffersFactory},
4143
{provide: KeyValueDiffers, useFactory: _keyValueDiffersFactory},
4244
{provide: LOCALE_ID, useValue: 'en-US'},

modules/@angular/core/src/linker/animation_view_context.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
*/
88
import {AnimationGroupPlayer} from '../animation/animation_group_player';
99
import {AnimationPlayer} from '../animation/animation_player';
10-
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
10+
import {AnimationQueue} from '../animation/animation_queue';
1111
import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
1212
import {ViewAnimationMap} from '../animation/view_animation_map';
13-
import {ListWrapper} from '../facade/collection';
1413

1514
export class AnimationViewContext {
1615
private _players = new ViewAnimationMap();
1716

17+
constructor(private _animationQueue: AnimationQueue) {}
18+
1819
onAllActiveAnimationsDone(callback: () => any): void {
1920
const activeAnimationPlayers = this._players.getAllPlayers();
2021
// we check for the length to avoid having GroupAnimationPlayer
@@ -27,7 +28,7 @@ export class AnimationViewContext {
2728
}
2829

2930
queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
30-
queueAnimationGlobally(player);
31+
this._animationQueue.enqueue(player);
3132
this._players.set(element, animationName, player);
3233
player.onDone(() => this._players.remove(element, animationName, player));
3334
}

modules/@angular/core/src/linker/view.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export abstract class AppView<T> {
6363
public viewUtils: ViewUtils, public parentView: AppView<any>, public parentIndex: number,
6464
public parentElement: any, public cdMode: ChangeDetectorStatus,
6565
public declaredViewContainer: ViewContainer = null) {
66-
this.ref = new ViewRef_(this);
66+
this.ref = new ViewRef_(this, viewUtils.animationQueue);
6767
if (type === ViewType.COMPONENT || type === ViewType.HOST) {
6868
this.renderer = viewUtils.renderComponent(componentType);
6969
} else {
@@ -74,7 +74,7 @@ export abstract class AppView<T> {
7474

7575
get animationContext(): AnimationViewContext {
7676
if (!this._animationContext) {
77-
this._animationContext = new AnimationViewContext();
77+
this._animationContext = new AnimationViewContext(this.viewUtils.animationQueue);
7878
}
7979
return this._animationContext;
8080
}

modules/@angular/core/src/linker/view_ref.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {triggerQueuedAnimations} from '../animation/animation_queue';
9+
import {AnimationQueue} from '../animation/animation_queue';
1010
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
1111
import {ChangeDetectorStatus} from '../change_detection/constants';
1212
import {unimplemented} from '../facade/errors';
13-
1413
import {AppView} from './view';
1514

16-
1715
/**
1816
* @stable
1917
*/
@@ -92,7 +90,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
9290
/** @internal */
9391
_originalMode: ChangeDetectorStatus;
9492

95-
constructor(private _view: AppView<C>) {
93+
constructor(private _view: AppView<C>, public animationQueue: AnimationQueue) {
9694
this._view = _view;
9795
this._originalMode = this._view.cdMode;
9896
}
@@ -109,7 +107,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
109107
detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; }
110108
detectChanges(): void {
111109
this._view.detectChanges(false);
112-
triggerQueuedAnimations();
110+
this.animationQueue.flush();
113111
}
114112
checkNoChanges(): void { this._view.detectChanges(true); }
115113
reattach(): void {

0 commit comments

Comments
 (0)