Skip to content

Commit

Permalink
perf(ivy): chain listener instructions (#33720) (#34340)
Browse files Browse the repository at this point in the history
Chains multiple listener instructions on a particular element into a single call which results in less generated code. Also handles listeners on templates, host listeners and synthetic host listeners.

PR Close #33720

PR Close #34340
  • Loading branch information
crisbeto authored and AndrewKushnir committed Dec 11, 2019
1 parent bbb9412 commit d3ec306
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 52 deletions.
107 changes: 107 additions & 0 deletions packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts
Expand Up @@ -1134,6 +1134,113 @@ describe('compiler compliance: bindings', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });


it('should chain multiple host listeners into a single instruction', () => {
const files = {
app: {
'example.ts': `
import {Directive, HostListener} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'(mousedown)': 'mousedown()',
'(mouseup)': 'mouseup()',
}
})
export class MyDirective {
mousedown() {}
mouseup() {}
@HostListener('click')
click() {}
}`
}
};

const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵlistener("mousedown", function MyDirective_mousedown_HostBindingHandler($event) { return ctx.mousedown(); })("mouseup", function MyDirective_mouseup_HostBindingHandler($event) { return ctx.mouseup(); })("click", function MyDirective_click_HostBindingHandler($event) { return ctx.click(); });
}
}
`;

expectEmit(result.source, template, 'Incorrect template');
});

it('should chain multiple synthetic host listeners into a single instruction', () => {
const files = {
app: {
'example.ts': `
import {Component, HostListener} from '@angular/core';
@Component({
selector: 'my-comp',
template: '',
host: {
'(@animation.done)': 'done()',
}
})
export class MyComponent {
@HostListener('@animation.start')
start() {}
}`
}
};

const result = compile(files, angularFiles);
const template = `
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵcomponentHostSyntheticListener("@animation.done", function MyComponent_animation_animation_done_HostBindingHandler($event) { return ctx.done(); })("@animation.start", function MyComponent_animation_animation_start_HostBindingHandler($event) { return ctx.start(); });
}
}
`;

expectEmit(result.source, template, 'Incorrect template');
});

it('should chain multiple regular and synthetic host listeners into two instructions', () => {
const files = {
app: {
'example.ts': `
import {Component, HostListener} from '@angular/core';
@Component({
selector: 'my-comp',
template: '',
host: {
'(mousedown)': 'mousedown()',
'(@animation.done)': 'done()',
'(mouseup)': 'mouseup()',
}
})
export class MyComponent {
@HostListener('@animation.start')
start() {}
@HostListener('click')
click() {}
}`
}
};

const result = compile(files, angularFiles);
const template = `
hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) {
if (rf & 1) {
$r3$.ɵɵcomponentHostSyntheticListener("@animation.done", function MyComponent_animation_animation_done_HostBindingHandler($event) { return ctx.done(); })("@animation.start", function MyComponent_animation_animation_start_HostBindingHandler($event) { return ctx.start(); });
$r3$.ɵɵlistener("mousedown", function MyComponent_mousedown_HostBindingHandler($event) { return ctx.mousedown(); })("mouseup", function MyComponent_mouseup_HostBindingHandler($event) { return ctx.mouseup(); })("click", function MyComponent_click_HostBindingHandler($event) { return ctx.click(); });
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});

}); });


describe('non bindable behavior', () => { describe('non bindable behavior', () => {
Expand Down
Expand Up @@ -229,4 +229,119 @@ describe('compiler compliance: listen()', () => {
expectEmit(source, MyComponentFactory, 'Incorrect MyComponent.ɵfac'); expectEmit(source, MyComponentFactory, 'Incorrect MyComponent.ɵfac');
}); });


it('should chain multiple listeners on the same element', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div (click)="click()" (change)="change()"></div>\`
})
export class MyComponent {
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
consts: [[${AttributeMarker.Bindings}, "click", "change"]],
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", 0);
$r3$.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener($event) {
return ctx.click();
})("change", function MyComponent_Template_div_change_0_listener($event) {
return ctx.change();
});
$r3$.ɵɵelementEnd();
}
}
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});

it('should chain multiple listeners across elements', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
<div (click)="click()" (change)="change()"></div>
<some-comp (update)="update()" (delete)="delete()"></some-comp>
\`
})
export class MyComponent {
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
consts: [[${AttributeMarker.Bindings}, "click", "change"], [${AttributeMarker.Bindings}, "update", "delete"]],
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", 0);
$r3$.ɵɵlistener("click", function MyComponent_Template_div_click_0_listener($event) { return ctx.click(); })("change", function MyComponent_Template_div_change_0_listener($event) { return ctx.change(); });
$r3$.ɵɵelementEnd();
$r3$.ɵɵelementStart(1, "some-comp", 1);
$r3$.ɵɵlistener("update", function MyComponent_Template_some_comp_update_1_listener($event) { return ctx.update(); })("delete", function MyComponent_Template_some_comp_delete_1_listener($event) { return ctx.delete(); });
$r3$.ɵɵelementEnd();
}
}
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});

it('should chain multiple listeners on the same template', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<ng-template (click)="click()" (change)="change()"></ng-template>\`
})
export class MyComponent {
}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};

const template = `
consts: [[${AttributeMarker.Bindings}, "click", "change"]],
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵtemplate(0, MyComponent_ng_template_0_Template, 0, 0, "ng-template", 0);
$r3$.ɵɵlistener("click", function MyComponent_Template_ng_template_click_0_listener($event) { return ctx.click(); })("change", function MyComponent_Template_ng_template_change_0_listener($event) { return ctx.change(); });
}
}
`;

const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});


}); });
Expand Up @@ -277,8 +277,7 @@ describe('compiler compliance: styling', () => {
template: function MyComponent_Template(rf, ctx) { template: function MyComponent_Template(rf, ctx) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵelementStart(0, "div"); $r3$.ɵɵelementStart(0, "div");
$r3$.ɵɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); }); $r3$.ɵɵlistener("@myAnimation.start", function MyComponent_Template_div_animation_myAnimation_start_0_listener($event) { return ctx.onStart($event); })("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); });
$r3$.ɵɵlistener("@myAnimation.done", function MyComponent_Template_div_animation_myAnimation_done_0_listener($event) { return ctx.onDone($event); });
$r3$.ɵɵelementEnd(); $r3$.ɵɵelementEnd();
} if (rf & 2) { } if (rf & 2) {
$r3$.ɵɵproperty("@myAnimation", ctx.exp); $r3$.ɵɵproperty("@myAnimation", ctx.exp);
Expand Down Expand Up @@ -337,8 +336,7 @@ describe('compiler compliance: styling', () => {
hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) { hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
$r3$.ɵɵallocHostVars(1); $r3$.ɵɵallocHostVars(1);
$r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); }); $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); })("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
$r3$.ɵɵcomponentHostSyntheticListener("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); });
} if (rf & 2) { } if (rf & 2) {
$r3$.ɵɵupdateSyntheticHostBinding("@myAnim", ctx.myAnimState); $r3$.ɵɵupdateSyntheticHostBinding("@myAnim", ctx.myAnimState);
} }
Expand Down
10 changes: 3 additions & 7 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Expand Up @@ -2186,9 +2186,7 @@ runInEachFileSystem(os => {
const hostBindingsFn = ` const hostBindingsFn = `
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); }); i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick(); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵɵresolveDocument)("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵɵresolveWindow);
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onDocumentClick($event.target); }, false, i0.ɵɵresolveDocument);
i0.ɵɵlistener("scroll", function FooCmp_scroll_HostBindingHandler($event) { return ctx.onWindowScroll(); }, false, i0.ɵɵresolveWindow);
} }
} }
`; `;
Expand Down Expand Up @@ -2281,9 +2279,7 @@ runInEachFileSystem(os => {
hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) {
if (rf & 1) { if (rf & 1) {
i0.ɵɵallocHostVars(3); i0.ɵɵallocHostVars(3);
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); }); i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody);
i0.ɵɵlistener("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); });
} }
if (rf & 2) { if (rf & 2) {
i0.ɵɵhostProperty("prop", ctx.bar); i0.ɵɵhostProperty("prop", ctx.bar);
Expand Down Expand Up @@ -3288,7 +3284,7 @@ runInEachFileSystem(os => {


env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({ @Component({
template: '<div #ref="unknownTarget"></div>', template: '<div #ref="unknownTarget"></div>',
}) })
Expand Down
26 changes: 22 additions & 4 deletions packages/compiler/src/render3/view/compiler.ts
Expand Up @@ -733,17 +733,35 @@ function getBindingNameAndInstruction(binding: ParsedProperty):
} }


function createHostListeners(eventBindings: ParsedEvent[], name?: string): o.Statement[] { function createHostListeners(eventBindings: ParsedEvent[], name?: string): o.Statement[] {
return eventBindings.map(binding => { const listeners: o.Expression[][] = [];
const syntheticListeners: o.Expression[][] = [];
const instructions: o.Statement[] = [];

eventBindings.forEach(binding => {
let bindingName = binding.name && sanitizeIdentifier(binding.name); let bindingName = binding.name && sanitizeIdentifier(binding.name);
const bindingFnName = binding.type === ParsedEventType.Animation ? const bindingFnName = binding.type === ParsedEventType.Animation ?
prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) : prepareSyntheticListenerFunctionName(bindingName, binding.targetOrPhase) :
bindingName; bindingName;
const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null; const handlerName = name && bindingName ? `${name}_${bindingFnName}_HostBindingHandler` : null;
const params = prepareEventListenerParameters(BoundEvent.fromParsedEvent(binding), handlerName); const params = prepareEventListenerParameters(BoundEvent.fromParsedEvent(binding), handlerName);
const instruction =
binding.type == ParsedEventType.Animation ? R3.componentHostSyntheticListener : R3.listener; if (binding.type == ParsedEventType.Animation) {
return o.importExpr(instruction).callFn(params).toStmt(); syntheticListeners.push(params);
} else {
listeners.push(params);
}
}); });

if (syntheticListeners.length > 0) {
instructions.push(
chainedInstruction(R3.componentHostSyntheticListener, syntheticListeners).toStmt());
}

if (listeners.length > 0) {
instructions.push(chainedInstruction(R3.listener, listeners).toStmt());
}

return instructions;
} }


function metadataAsSummary(meta: R3HostMetadata): CompileDirectiveSummary { function metadataAsSummary(meta: R3HostMetadata): CompileDirectiveSummary {
Expand Down
36 changes: 26 additions & 10 deletions packages/compiler/src/render3/view/template.ts
Expand Up @@ -684,11 +684,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
} }


// Generate Listeners (outputs) // Generate Listeners (outputs)
element.outputs.forEach((outputAst: t.BoundEvent) => { if (element.outputs.length > 0) {
this.creationInstruction( const listeners = element.outputs.map(
outputAst.sourceSpan, R3.listener, (outputAst: t.BoundEvent) => ({
this.prepareListenerParameter(element.name, outputAst, elementIndex)); sourceSpan: outputAst.sourceSpan,
}); params: this.prepareListenerParameter(element.name, outputAst, elementIndex)
}));
this.creationInstructionChain(R3.listener, listeners);
}


// Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and // Note: it's important to keep i18n/i18nStart instructions after i18nAttributes and
// listeners, to make sure i18nAttributes instruction targets current element at runtime. // listeners, to make sure i18nAttributes instruction targets current element at runtime.
Expand Down Expand Up @@ -918,11 +921,14 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// Add the input bindings // Add the input bindings
this.templatePropertyBindings(templateIndex, template.inputs); this.templatePropertyBindings(templateIndex, template.inputs);
// Generate listeners for directive output // Generate listeners for directive output
template.outputs.forEach((outputAst: t.BoundEvent) => { if (template.outputs.length > 0) {
this.creationInstruction( const listeners = template.outputs.map(
outputAst.sourceSpan, R3.listener, (outputAst: t.BoundEvent) => ({
this.prepareListenerParameter('ng_template', outputAst, templateIndex)); sourceSpan: outputAst.sourceSpan,
}); params: this.prepareListenerParameter('ng_template', outputAst, templateIndex)
}));
this.creationInstructionChain(R3.listener, listeners);
}
} }
} }


Expand Down Expand Up @@ -1098,6 +1104,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || [], prepend); this.instructionFn(this._creationCodeFns, span, reference, paramsOrFn || [], prepend);
} }


private creationInstructionChain(reference: o.ExternalReference, calls: {
sourceSpan: ParseSourceSpan | null,
params: () => o.Expression[]
}[]) {
const span = calls.length ? calls[0].sourceSpan : null;
this._creationCodeFns.push(() => {
return chainedInstruction(reference, calls.map(call => call.params()), span).toStmt();
});
}

private updateInstructionWithAdvance( private updateInstructionWithAdvance(
nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference, nodeIndex: number, span: ParseSourceSpan|null, reference: o.ExternalReference,
paramsOrFn?: o.Expression[]|(() => o.Expression[])) { paramsOrFn?: o.Expression[]|(() => o.Expression[])) {
Expand Down

0 comments on commit d3ec306

Please sign in to comment.