Skip to content

Commit

Permalink
perf(animations): always run the animation queue outside of zones
Browse files Browse the repository at this point in the history
Related #12732
Closes #13440
  • Loading branch information
matsko authored and vicb committed Dec 15, 2016
1 parent ecfad46 commit e2622ad
Show file tree
Hide file tree
Showing 17 changed files with 439 additions and 104 deletions.
45 changes: 27 additions & 18 deletions modules/@angular/compiler/src/compiler_util/render_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ function sanitizedValue(

export function triggerAnimation(
view: o.Expression, componentView: o.Expression, boundProp: BoundElementPropertyAst,
eventListener: o.Expression, renderElement: o.Expression, renderValue: o.Expression,
lastRenderValue: o.Expression) {
boundOutputs: BoundEventAst[], eventListener: o.Expression, renderElement: o.Expression,
renderValue: o.Expression, lastRenderValue: o.Expression) {
const detachStmts: o.Statement[] = [];
const updateStmts: o.Statement[] = [];

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

const registerStmts = [
animationTransitionVar
.callMethod(
'onStart',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
.toStmt(),
animationTransitionVar
.callMethod(
'onDone',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
.toStmt(),
const registerStmts: o.Statement[] = [];
const animationStartMethodExists = boundOutputs.find(
event => event.isAnimation && event.name == animationName && event.phase == 'start');
if (animationStartMethodExists) {
registerStmts.push(
animationTransitionVar
.callMethod(
'onStart',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'start'))])])
.toStmt());
}

];
const animationDoneMethodExists = boundOutputs.find(
event => event.isAnimation && event.name == animationName && event.phase == 'done');
if (animationDoneMethodExists) {
registerStmts.push(
animationTransitionVar
.callMethod(
'onDone',
[eventListener.callMethod(
o.BuiltinMethod.Bind,
[view, o.literal(BoundEventAst.calcFullName(animationName, null, 'done'))])])
.toStmt());
}

updateStmts.push(...registerStmts);
detachStmts.push(...registerStmts);
Expand Down
7 changes: 4 additions & 3 deletions modules/@angular/compiler/src/directive_wrapper_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class DirectiveWrapperCompiler {
addCheckInputMethod(inputFieldName, builder);
});
addNgDoCheckMethod(builder);
addCheckHostMethod(hostParseResult.hostProps, builder);
addCheckHostMethod(hostParseResult.hostProps, hostParseResult.hostListeners, builder);
addHandleEventMethod(hostParseResult.hostListeners, builder);
addSubscribeMethod(dirMeta, builder);

Expand Down Expand Up @@ -235,7 +235,8 @@ function addCheckInputMethod(input: string, builder: DirectiveWrapperBuilder) {
}

function addCheckHostMethod(
hostProps: BoundElementPropertyAst[], builder: DirectiveWrapperBuilder) {
hostProps: BoundElementPropertyAst[], hostEvents: BoundEventAst[],
builder: DirectiveWrapperBuilder) {
const stmts: o.Statement[] = [];
const methodParams: o.FnParam[] = [
new o.FnParam(
Expand All @@ -262,7 +263,7 @@ function addCheckHostMethod(
let checkBindingStmts: o.Statement[];
if (hostProp.isAnimation) {
const {updateStmts, detachStmts} = triggerAnimation(
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp,
VIEW_VAR, COMPONENT_VIEW_VAR, hostProp, hostEvents,
o.THIS_EXPR.prop(EVENT_HANDLER_FIELD_NAME)
.or(o.importExpr(createIdentifier(Identifiers.noop))),
RENDER_EL_VAR, evalResult.currValExpr, field.expression);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {Identifiers, createIdentifier} from '../identifiers';
import * as o from '../output/output_ast';
import {isDefaultChangeDetectionStrategy} from '../private_import_core';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {BoundElementPropertyAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
import {BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, PropertyBindingType} from '../template_parser/template_ast';
import {CompileElement, CompileNode} from './compile_element';
import {CompileView} from './compile_view';
import {DetectChangesVars} from './constants';
Expand All @@ -41,7 +41,8 @@ export function bindRenderText(
}

export function bindRenderInputs(
boundProps: BoundElementPropertyAst[], hasEvents: boolean, compileElement: CompileElement) {
boundProps: BoundElementPropertyAst[], boundOutputs: BoundEventAst[], hasEvents: boolean,
compileElement: CompileElement) {
const view = compileElement.view;
const renderNode = compileElement.renderNode;

Expand All @@ -67,7 +68,7 @@ export function bindRenderInputs(
case PropertyBindingType.Animation:
compileMethod = view.animationBindingsMethod;
const {updateStmts, detachStmts} = triggerAnimation(
o.THIS_EXPR, o.THIS_EXPR, boundProp,
o.THIS_EXPR, o.THIS_EXPR, boundProp, boundOutputs,
(hasEvents ? o.THIS_EXPR.prop(getHandleEventMethodName(compileElement.nodeIndex)) :
o.importExpr(createIdentifier(Identifiers.noop)))
.callMethod(o.BuiltinMethod.Bind, [o.THIS_EXPR]),
Expand Down
2 changes: 1 addition & 1 deletion modules/@angular/compiler/src/view_compiler/view_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ViewBinderVisitor implements TemplateAstVisitor {
visitElement(ast: ElementAst, parent: CompileElement): any {
const compileElement = <CompileElement>this.view.nodes[this._nodeIndex++];
const hasEvents = bindOutputs(ast.outputs, ast.directives, compileElement, true);
bindRenderInputs(ast.inputs, hasEvents, compileElement);
bindRenderInputs(ast.inputs, ast.outputs, hasEvents, compileElement);
ast.directives.forEach((directiveAst, dirIndex) => {
const directiveWrapperInstance =
compileElement.directiveWrapperInstance.get(directiveAst.directive.type.reference);
Expand Down
55 changes: 36 additions & 19 deletions modules/@angular/core/src/animation/animation_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,47 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Injectable} from '../di/metadata';
import {NgZone} from '../zone/ng_zone';
import {AnimationPlayer} from './animation_player';

let _queuedAnimations: AnimationPlayer[] = [];
@Injectable()
export class AnimationQueue {
public entries: AnimationPlayer[] = [];

/** @internal */
export function queueAnimation(player: AnimationPlayer) {
_queuedAnimations.push(player);
}
constructor(private _zone: NgZone) {}

enqueue(player: AnimationPlayer) { this.entries.push(player); }

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

function _triggerAnimations() {
for (let i = 0; i < _queuedAnimations.length; i++) {
const player = _queuedAnimations[i];
player.play();
private _triggerAnimations() {
NgZone.assertNotInAngularZone();

while (this.entries.length) {
const player = this.entries.shift();
// in the event that an animation throws an error then we do
// not want to re-run animations on any previous animations
// if they have already been kicked off beforehand
if (!player.hasStarted()) {
player.play();
}
}
}
_queuedAnimations = [];
}
10 changes: 6 additions & 4 deletions modules/@angular/core/src/animation/animation_transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export class AnimationTransition {
}

onStart(callback: (event: AnimationTransitionEvent) => any): void {
const event = this._createEvent('start');
this._player.onStart(() => callback(event));
const fn =
<() => void>Zone.current.wrap(() => callback(this._createEvent('start')), 'player.onStart');
this._player.onStart(fn);
}

onDone(callback: (event: AnimationTransitionEvent) => any): void {
const event = this._createEvent('done');
this._player.onDone(() => callback(event));
const fn =
<() => void>Zone.current.wrap(() => callback(this._createEvent('done')), 'player.onDone');
this._player.onDone(fn);
}
}
2 changes: 2 additions & 0 deletions modules/@angular/core/src/application_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AnimationQueue} from './animation/animation_queue';
import {ApplicationInitStatus} from './application_init';
import {ApplicationRef, ApplicationRef_} from './application_ref';
import {APP_ID_RANDOM_PROVIDER} from './application_tokens';
Expand Down Expand Up @@ -37,6 +38,7 @@ export function _keyValueDiffersFactory() {
Compiler,
APP_ID_RANDOM_PROVIDER,
ViewUtils,
AnimationQueue,
{provide: IterableDiffers, useFactory: _iterableDiffersFactory},
{provide: KeyValueDiffers, useFactory: _keyValueDiffersFactory},
{provide: LOCALE_ID, useValue: 'en-US'},
Expand Down
7 changes: 4 additions & 3 deletions modules/@angular/core/src/linker/animation_view_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
*/
import {AnimationGroupPlayer} from '../animation/animation_group_player';
import {AnimationPlayer} from '../animation/animation_player';
import {queueAnimation as queueAnimationGlobally} from '../animation/animation_queue';
import {AnimationQueue} from '../animation/animation_queue';
import {AnimationSequencePlayer} from '../animation/animation_sequence_player';
import {ViewAnimationMap} from '../animation/view_animation_map';
import {ListWrapper} from '../facade/collection';

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

constructor(private _animationQueue: AnimationQueue) {}

onAllActiveAnimationsDone(callback: () => any): void {
const activeAnimationPlayers = this._players.getAllPlayers();
// we check for the length to avoid having GroupAnimationPlayer
Expand All @@ -27,7 +28,7 @@ export class AnimationViewContext {
}

queueAnimation(element: any, animationName: string, player: AnimationPlayer): void {
queueAnimationGlobally(player);
this._animationQueue.enqueue(player);
this._players.set(element, animationName, player);
player.onDone(() => this._players.remove(element, animationName, player));
}
Expand Down
4 changes: 2 additions & 2 deletions modules/@angular/core/src/linker/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export abstract class AppView<T> {
public viewUtils: ViewUtils, public parentView: AppView<any>, public parentIndex: number,
public parentElement: any, public cdMode: ChangeDetectorStatus,
public declaredViewContainer: ViewContainer = null) {
this.ref = new ViewRef_(this);
this.ref = new ViewRef_(this, viewUtils.animationQueue);
if (type === ViewType.COMPONENT || type === ViewType.HOST) {
this.renderer = viewUtils.renderComponent(componentType);
} else {
Expand All @@ -74,7 +74,7 @@ export abstract class AppView<T> {

get animationContext(): AnimationViewContext {
if (!this._animationContext) {
this._animationContext = new AnimationViewContext();
this._animationContext = new AnimationViewContext(this.viewUtils.animationQueue);
}
return this._animationContext;
}
Expand Down
8 changes: 3 additions & 5 deletions modules/@angular/core/src/linker/view_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import {triggerQueuedAnimations} from '../animation/animation_queue';
import {AnimationQueue} from '../animation/animation_queue';
import {ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {ChangeDetectorStatus} from '../change_detection/constants';
import {unimplemented} from '../facade/errors';

import {AppView} from './view';


/**
* @stable
*/
Expand Down Expand Up @@ -92,7 +90,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
/** @internal */
_originalMode: ChangeDetectorStatus;

constructor(private _view: AppView<C>) {
constructor(private _view: AppView<C>, public animationQueue: AnimationQueue) {
this._view = _view;
this._originalMode = this._view.cdMode;
}
Expand All @@ -109,7 +107,7 @@ export class ViewRef_<C> implements EmbeddedViewRef<C>, ChangeDetectorRef {
detach(): void { this._view.cdMode = ChangeDetectorStatus.Detached; }
detectChanges(): void {
this._view.detectChanges(false);
triggerQueuedAnimations();
this.animationQueue.flush();
}
checkNoChanges(): void { this._view.detectChanges(true); }
reattach(): void {
Expand Down
9 changes: 7 additions & 2 deletions modules/@angular/core/src/linker/view_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {AnimationQueue} from '../animation/animation_queue';
import {SimpleChange, devModeEqual} from '../change_detection/change_detection';
import {UNINITIALIZED} from '../change_detection/change_detection_util';
import {Inject, Injectable} from '../di';
Expand All @@ -14,17 +15,21 @@ import {ViewEncapsulation} from '../metadata/view';
import {RenderComponentType, RenderDebugInfo, Renderer, RootRenderer} from '../render/api';
import {Sanitizer} from '../security';
import {VERSION} from '../version';
import {NgZone} from '../zone/ng_zone';

import {ExpressionChangedAfterItHasBeenCheckedError} from './errors';
import {AppView} from './view';
import {ViewContainer} from './view_container';

@Injectable()
export class ViewUtils {
sanitizer: Sanitizer;
private _nextCompTypeId: number = 0;

constructor(private _renderer: RootRenderer, sanitizer: Sanitizer) { this.sanitizer = sanitizer; }
constructor(
private _renderer: RootRenderer, sanitizer: Sanitizer,
public animationQueue: AnimationQueue) {
this.sanitizer = sanitizer;
}

/** @internal */
renderComponent(renderComponentType: RenderComponentType): Renderer {
Expand Down
Loading

0 comments on commit e2622ad

Please sign in to comment.