perf(ivy): chain host binding instructions (#31296)

Adds chaining to the `property`, `attribute` and `updateSyntheticHostBinding` instructions when they're used in a host binding.

This PR resolves FW-1404.

PR Close #31296
This commit is contained in:
crisbeto 2019-06-27 20:23:15 +02:00 committed by Alex Rickabaugh
parent c6b29f4c6d
commit 81332150aa
9 changed files with 309 additions and 49 deletions

View File

@ -443,8 +443,7 @@ describe('compiler compliance', () => {
$r3$.ɵɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight) $r3$.ɵɵpureFunction2(2, $_c0$, ctx.collapsedHeight, ctx.expandedHeight)
) )
, null, true , null, true
); )("@expansionWidth",
$r3$.ɵɵupdateSyntheticHostBinding("@expansionWidth",
$r3$.ɵɵpureFunction2(11, $_c1$, ctx.getExpandedState(), $r3$.ɵɵpureFunction2(11, $_c1$, ctx.getExpandedState(),
$r3$.ɵɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth) $r3$.ɵɵpureFunction2(8, $_c2$, ctx.collapsedWidth, ctx.expandedWidth)
) )

View File

@ -210,7 +210,7 @@ describe('compiler compliance: bindings', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should chain property bindings in the presence of other instructions', () => { it('should chain property bindings in the presence of other bindings', () => {
const files = { const files = {
app: { app: {
'example.ts': ` 'example.ts': `
@ -473,7 +473,7 @@ describe('compiler compliance: bindings', () => {
expectEmit(result.source, template, 'Incorrect template'); expectEmit(result.source, template, 'Incorrect template');
}); });
it('should chain attribute bindings in the presence of other instructions', () => { it('should chain attribute bindings in the presence of other bindings', () => {
const files = { const files = {
app: { app: {
'example.ts': ` 'example.ts': `
@ -860,6 +860,250 @@ describe('compiler compliance: bindings', () => {
const source = result.source; const source = result.source;
expectEmit(source, CompAndDirDeclaration, 'Invalid host attribute code'); expectEmit(source, CompAndDirDeclaration, 'Invalid host attribute code');
}); });
it('should chain multiple host property bindings into a single instruction', () => {
const files = {
app: {
'example.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[title]': 'myTitle',
'[tabindex]': '1',
'[id]': 'myId'
}
})
export class MyDirective {
myTitle = 'hello';
myId = 'special-directive';
}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵproperty("title", ctx.myTitle, null, true)("tabindex", 1, null, true)("id", ctx.myId, null, true);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain both host properties in the decorator and on the class', () => {
const files = {
app: {
'example.ts': `
import {Directive, HostBinding} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[tabindex]': '1'
}
})
export class MyDirective {
@HostBinding('title')
myTitle = 'hello';
@HostBinding('id')
myId = 'special-directive';
}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵproperty("tabindex", 1, null, true)("title", ctx.myTitle, null, true)("id", ctx.myId, null, true);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain multiple host property bindings in the presence of other bindings', () => {
const files = {
app: {
'example.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[title]': '"my title"',
'[attr.tabindex]': '1',
'[id]': '"my-id"'
}
})
export class MyDirective {}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵproperty("title", "my title", null, true)("id", "my-id", null, true);
$r3$.ɵɵattribute("tabindex", 1);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain multiple synthetic properties into a single instruction call', () => {
const files = {
app: {
'example.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[@expand]': 'expandedState',
'[@fadeOut]': 'true',
'[@shrink]': 'isSmall'
}
})
export class MyDirective {
expandedState = 'collapsed';
isSmall = true;
}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵupdateSyntheticHostBinding("@expand", ctx.expandedState, null, true)("@fadeOut", true, null, true)("@shrink", ctx.isSmall, null, true);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain multiple host attribute bindings into a single instruction', () => {
const files = {
app: {
'example.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[attr.title]': 'myTitle',
'[attr.tabindex]': '1',
'[attr.id]': 'myId'
}
})
export class MyDirective {
myTitle = 'hello';
myId = 'special-directive';
}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵattribute("title", ctx.myTitle)("tabindex", 1)("id", ctx.myId);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain both host attributes in the decorator and on the class', () => {
const files = {
app: {
'example.ts': `
import {Directive, HostBinding} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[attr.tabindex]': '1'
}
})
export class MyDirective {
@HostBinding('attr.title')
myTitle = 'hello';
@HostBinding('attr.id')
myId = 'special-directive';
}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵattribute("tabindex", 1)("title", ctx.myTitle)("id", ctx.myId);
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
it('should chain multiple host attribute bindings in the presence of other bindings', () => {
const files = {
app: {
'example.ts': `
import {Directive} from '@angular/core';
@Directive({
selector: '[my-dir]',
host: {
'[attr.title]': '"my title"',
'[tabindex]': '1',
'[attr.id]': '"my-id"'
}
})
export class MyDirective {}`
}
};
const result = compile(files, angularFiles);
const template = `
hostBindings: function MyDirective_HostBindings(rf, ctx, elIndex) {
if (rf & 2) {
$r3$.ɵɵproperty("tabindex", 1, null, true);
$r3$.ɵɵattribute("title", "my title")("id", "my-id");
}
}
`;
expectEmit(result.source, template, 'Incorrect template');
});
}); });
describe('non bindable behavior', () => { describe('non bindable behavior', () => {

View File

@ -1388,8 +1388,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyling(null, null, $r3$.ɵɵdefaultStyleSanitizer);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵproperty("id", ctx.id, null, true); $r3$.ɵɵproperty("id", ctx.id, null, true)("title", ctx.title, null, true);
$r3$.ɵɵproperty("title", ctx.title, null, true);
$r3$.ɵɵstyleMap(ctx.myStyle); $r3$.ɵɵstyleMap(ctx.myStyle);
$r3$.ɵɵclassMap(ctx.myClass); $r3$.ɵɵclassMap(ctx.myClass);
$r3$.ɵɵstylingApply(); $r3$.ɵɵstylingApply();
@ -1435,8 +1434,7 @@ describe('compiler compliance: styling', () => {
$r3$.ɵɵstyling($_c0$, $_c1$); $r3$.ɵɵstyling($_c0$, $_c1$);
} }
if (rf & 2) { if (rf & 2) {
$r3$.ɵɵproperty("id", ctx.id, null, true); $r3$.ɵɵproperty("id", ctx.id, null, true)("title", ctx.title, null, true);
$r3$.ɵɵproperty("title", ctx.title, null, true);
$r3$.ɵɵstyleProp(0, ctx.myWidth); $r3$.ɵɵstyleProp(0, ctx.myWidth);
$r3$.ɵɵclassProp(0, ctx.myFooClass); $r3$.ɵɵclassProp(0, ctx.myFooClass);
$r3$.ɵɵstylingApply(); $r3$.ɵɵstylingApply();

View File

@ -1795,8 +1795,8 @@ runInEachFileSystem(os => {
i0.ɵɵstyling(_c0); i0.ɵɵstyling(_c0);
} }
if (rf & 2) { if (rf & 2) {
i0.ɵɵattribute("hello", ctx.foo);
i0.ɵɵproperty("prop", ctx.bar, null, true); i0.ɵɵproperty("prop", ctx.bar, null, true);
i0.ɵɵattribute("hello", ctx.foo);
i0.ɵɵclassProp(0, ctx.someClass); i0.ɵɵclassProp(0, ctx.someClass);
i0.ɵɵstylingApply(); i0.ɵɵstylingApply();
} }
@ -3236,12 +3236,7 @@ runInEachFileSystem(os => {
i0.ɵɵallocHostVars(6); i0.ɵɵallocHostVars(6);
} }
if (rf & 2) { if (rf & 2) {
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl); i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
i0.ɵɵattribute("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl);
i0.ɵɵattribute("action", ctx.attrAction, i0.ɵɵsanitizeUrl);
i0.ɵɵattribute("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl);
i0.ɵɵattribute("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml);
i0.ɵɵattribute("title", ctx.attrSafeTitle);
} }
} }
`; `;
@ -3291,12 +3286,7 @@ runInEachFileSystem(os => {
i0.ɵɵallocHostVars(6); i0.ɵɵallocHostVars(6);
} }
if (rf & 2) { if (rf & 2) {
i0.ɵɵproperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl, true); i0.ɵɵproperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl, true)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl, true)("action", ctx.propAction, i0.ɵɵsanitizeUrl, true)("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl, true)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml, true)("title", ctx.propSafeTitle, null, true);
i0.ɵɵproperty("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl, true);
i0.ɵɵproperty("action", ctx.propAction, i0.ɵɵsanitizeUrl, true);
i0.ɵɵproperty("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl, true);
i0.ɵɵproperty("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml, true);
i0.ɵɵproperty("title", ctx.propSafeTitle, null, true);
} }
} }
`; `;
@ -3331,12 +3321,8 @@ runInEachFileSystem(os => {
i0.ɵɵallocHostVars(6); i0.ɵɵallocHostVars(6);
} }
if (rf & 2) { if (rf & 2) {
i0.ɵɵproperty("src", ctx.srcProp, null, true); i0.ɵɵproperty("src", ctx.srcProp, null, true)("href", ctx.hrefProp, null, true)("title", ctx.titleProp, null, true);
i0.ɵɵproperty("href", ctx.hrefProp, null, true); i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr)("title", ctx.titleAttr);
i0.ɵɵproperty("title", ctx.titleProp, null, true);
i0.ɵɵattribute("src", ctx.srcAttr);
i0.ɵɵattribute("href", ctx.hrefAttr);
i0.ɵɵattribute("title", ctx.titleAttr);
} }
} }
`; `;

View File

@ -30,7 +30,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, type
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {Instruction, StylingBuilder} from './styling_builder'; import {Instruction, StylingBuilder} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
const EMPTY_ARRAY: any[] = []; const EMPTY_ARRAY: any[] = [];
@ -638,6 +638,10 @@ function createHostBindingsFunction(
// Calculate the host property bindings // Calculate the host property bindings
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan); const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
const propertyBindings: o.Expression[][] = [];
const attributeBindings: o.Expression[][] = [];
const syntheticHostBindings: o.Expression[][] = [];
(bindings || []).forEach((binding: ParsedProperty) => { (bindings || []).forEach((binding: ParsedProperty) => {
const name = binding.name; const name = binding.name;
const stylingInputWasSet = const stylingInputWasSet =
@ -681,10 +685,32 @@ function createHostBindingsFunction(
} }
updateStatements.push(...bindingExpr.stmts); updateStatements.push(...bindingExpr.stmts);
updateStatements.push(o.importExpr(instruction).callFn(instructionParams).toStmt());
if (instruction === R3.property) {
propertyBindings.push(instructionParams);
} else if (instruction === R3.attribute) {
attributeBindings.push(instructionParams);
} else if (instruction === R3.updateSyntheticHostBinding) {
syntheticHostBindings.push(instructionParams);
} else {
updateStatements.push(o.importExpr(instruction).callFn(instructionParams).toStmt());
}
} }
}); });
if (propertyBindings.length > 0) {
updateStatements.push(chainedInstruction(R3.property, propertyBindings).toStmt());
}
if (attributeBindings.length > 0) {
updateStatements.push(chainedInstruction(R3.attribute, attributeBindings).toStmt());
}
if (syntheticHostBindings.length > 0) {
updateStatements.push(
chainedInstruction(R3.updateSyntheticHostBinding, syntheticHostBindings).toStmt());
}
// since we're dealing with directives/components and both have hostBinding // since we're dealing with directives/components and both have hostBinding
// functions, we need to generate a special hostAttrs instruction that deals // functions, we need to generate a special hostAttrs instruction that deals
// with both the assignment of styling as well as static attributes to the host // with both the assignment of styling as well as static attributes to the host

View File

@ -37,7 +37,8 @@ import {I18nMetaVisitor} from './i18n/meta';
import {getSerializedI18nContent} from './i18n/serializer'; import {getSerializedI18nContent} from './i18n/serializer';
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util'; import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
import {Instruction, StylingBuilder} from './styling_builder'; import {Instruction, StylingBuilder} from './styling_builder';
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util'; import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, chainedInstruction, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
// Selector attribute name of `<ng-content>` // Selector attribute name of `<ng-content>`
@ -1105,7 +1106,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
return fnParams; return fnParams;
}); });
return chainedInstruction(span, reference, calls).toStmt(); return chainedInstruction(reference, calls, span).toStmt();
}); });
} }
@ -1417,22 +1418,6 @@ function instruction(
return o.importExpr(reference, null, span).callFn(params, span); return o.importExpr(reference, null, span).callFn(params, span);
} }
function chainedInstruction(
span: ParseSourceSpan | null, reference: o.ExternalReference, calls: o.Expression[][]) {
let expression = o.importExpr(reference, null, span) as o.Expression;
if (calls.length > 0) {
for (let i = 0; i < calls.length; i++) {
expression = expression.callFn(calls[i], span);
}
} else {
// Add a blank invocation, in case the `calls` array is empty.
expression = expression.callFn([], span);
}
return expression;
}
// e.g. x(2); // e.g. x(2);
function generateNextContextExpr(relativeLevelDiff: number): o.Expression { function generateNextContextExpr(relativeLevelDiff: number): o.Expression {
return o.importExpr(R3.nextContext) return o.importExpr(R3.nextContext)

View File

@ -8,11 +8,14 @@
import {ConstantPool} from '../../constant_pool'; import {ConstantPool} from '../../constant_pool';
import * as o from '../../output/output_ast'; import * as o from '../../output/output_ast';
import {ParseSourceSpan} from '../../parse_util';
import {splitAtColon} from '../../util'; import {splitAtColon} from '../../util';
import * as t from '../r3_ast'; import * as t from '../r3_ast';
import {R3QueryMetadata} from './api'; import {R3QueryMetadata} from './api';
import {isI18nAttribute} from './i18n/util'; import {isI18nAttribute} from './i18n/util';
/** /**
* Checks whether an object key contains potentially unsafe chars, thus the key should be wrapped in * Checks whether an object key contains potentially unsafe chars, thus the key should be wrapped in
* quotes. Note: we do not wrap all keys into quotes, as it may have impact on minification and may * quotes. Note: we do not wrap all keys into quotes, as it may have impact on minification and may
@ -181,3 +184,20 @@ export function getAttrsForDirectiveMatching(elOrTpl: t.Element | t.Template):
return attributesMap; return attributesMap;
} }
/** Returns a call expression to a chained instruction, e.g. `property(params[0])(params[1])`. */
export function chainedInstruction(
reference: o.ExternalReference, calls: o.Expression[][], span?: ParseSourceSpan | null) {
let expression = o.importExpr(reference, null, span) as o.Expression;
if (calls.length > 0) {
for (let i = 0; i < calls.length; i++) {
expression = expression.callFn(calls[i], span);
}
} else {
// Add a blank invocation, in case the `calls` array is empty.
expression = expression.callFn([], span);
}
return expression;
}

View File

@ -84,7 +84,8 @@ export function bind<T>(lView: LView, value: T): T|NO_CHANGE {
* @codeGenApi * @codeGenApi
*/ */
export function ɵɵupdateSyntheticHostBinding<T>( export function ɵɵupdateSyntheticHostBinding<T>(
propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean) { propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null,
nativeOnly?: boolean): TsickleIssue1009 {
const index = getSelectedIndex(); const index = getSelectedIndex();
const lView = getLView(); const lView = getLView();
// TODO(benlesh): remove bind call here. // TODO(benlesh): remove bind call here.
@ -92,4 +93,5 @@ export function ɵɵupdateSyntheticHostBinding<T>(
if (bound !== NO_CHANGE) { if (bound !== NO_CHANGE) {
elementPropertyInternal(index, propName, bound, sanitizer, nativeOnly, loadComponentRenderer); elementPropertyInternal(index, propName, bound, sanitizer, nativeOnly, loadComponentRenderer);
} }
return ɵɵupdateSyntheticHostBinding;
} }

View File

@ -1075,7 +1075,7 @@ export declare function ɵɵtextInterpolate8(prefix: string, v0: any, i0: string
export declare function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009; export declare function ɵɵtextInterpolateV(values: any[]): TsickleIssue1009;
export declare function ɵɵupdateSyntheticHostBinding<T>(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): void; export declare function ɵɵupdateSyntheticHostBinding<T>(propName: string, value: T | NO_CHANGE, sanitizer?: SanitizerFn | null, nativeOnly?: boolean): TsickleIssue1009;
export declare function ɵɵviewQuery<T>(predicate: Type<any> | string[], descend: boolean, read: any): QueryList<T>; export declare function ɵɵviewQuery<T>(predicate: Type<any> | string[], descend: boolean, read: any): QueryList<T>;