refactor(ivy): generate 2 slots per styling instruction (#34616)

Compiler keeps track of number of slots (`vars`) which are needed for binding instructions. Normally each binding instructions allocates a single slot in the `LView` but styling instructions need to allocate two slots.

PR Close #34616
This commit is contained in:
Matias Niemelä
2019-12-14 19:48:24 -08:00
committed by Miško Hevery
parent b7ff38b1ef
commit 4005815114
6 changed files with 243 additions and 129 deletions

View File

@ -113,8 +113,6 @@ export class Identifiers {
static stylePropInterpolateV:
o.ExternalReference = {name: 'ɵɵstylePropInterpolateV', moduleName: CORE};
static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE};
static containerCreate: o.ExternalReference = {name: 'ɵɵcontainer', moduleName: CORE};
static nextContext: o.ExternalReference = {name: 'ɵɵnextContext', moduleName: CORE};

View File

@ -28,7 +28,7 @@ import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {StylingBuilder, StylingInstructionCall} from './styling_builder';
import {MIN_STYLING_BINDING_SLOTS_REQUIRED, StylingBuilder, StylingInstructionCall} from './styling_builder';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn} from './template';
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
@ -530,8 +530,6 @@ function createHostBindingsFunction(
hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan,
bindingParser: BindingParser, constantPool: ConstantPool, selector: string, name: string,
definitionMap: DefinitionMap): o.Expression|null {
// Initialize hostVarsCount to number of bound host properties (interpolations illegal)
const hostVarsCount = Object.keys(hostBindingsMetadata.properties).length;
const elVarExp = o.variable('elIndex');
const bindingContext = o.variable(CONTEXT_NAME);
const styleBuilder = new StylingBuilder(elVarExp, bindingContext);
@ -547,10 +545,38 @@ function createHostBindingsFunction(
const createStatements: o.Statement[] = [];
const updateStatements: o.Statement[] = [];
let totalHostVarsCount = hostVarsCount;
const hostBindingSourceSpan = typeSourceSpan;
const directiveSummary = metadataAsSummary(hostBindingsMetadata);
// Calculate host event bindings
const eventBindings =
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
if (eventBindings && eventBindings.length) {
const listeners = createHostListeners(eventBindings, name);
createStatements.push(...listeners);
}
// Calculate the host property bindings
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
const allOtherBindings: ParsedProperty[] = [];
// We need to calculate the total amount of binding slots required by
// all the instructions together before any value conversions happen.
// Value conversions may require additional slots for interpolation and
// bindings with pipes. These calculates happen after this block.
let totalHostVarsCount = 0;
bindings && bindings.forEach((binding: ParsedProperty) => {
const name = binding.name;
const stylingInputWasSet =
styleBuilder.registerInputBasedOnName(name, binding.expression, binding.sourceSpan);
if (stylingInputWasSet) {
totalHostVarsCount += MIN_STYLING_BINDING_SLOTS_REQUIRED;
} else {
allOtherBindings.push(binding);
totalHostVarsCount++;
}
});
let valueConverter: ValueConverter;
const getValueConverter = () => {
if (!valueConverter) {
@ -568,66 +594,50 @@ function createHostBindingsFunction(
return valueConverter;
};
// Calculate host event bindings
const eventBindings =
bindingParser.createDirectiveHostEventAsts(directiveSummary, hostBindingSourceSpan);
if (eventBindings && eventBindings.length) {
const listeners = createHostListeners(eventBindings, name);
createStatements.push(...listeners);
}
// Calculate the host property bindings
const bindings = bindingParser.createBoundHostProperties(directiveSummary, hostBindingSourceSpan);
const propertyBindings: o.Expression[][] = [];
const attributeBindings: o.Expression[][] = [];
const syntheticHostBindings: o.Expression[][] = [];
allOtherBindings.forEach((binding: ParsedProperty) => {
// resolve literal arrays and literal objects
const value = binding.expression.visit(getValueConverter());
const bindingExpr = bindingFn(bindingContext, value);
bindings && bindings.forEach((binding: ParsedProperty) => {
const name = binding.name;
const stylingInputWasSet =
styleBuilder.registerInputBasedOnName(name, binding.expression, binding.sourceSpan);
if (!stylingInputWasSet) {
// resolve literal arrays and literal objects
const value = binding.expression.visit(getValueConverter());
const bindingExpr = bindingFn(bindingContext, value);
const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding);
const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding);
const securityContexts =
bindingParser.calcPossibleSecurityContexts(selector, bindingName, isAttribute)
.filter(context => context !== core.SecurityContext.NONE);
const securityContexts =
bindingParser.calcPossibleSecurityContexts(selector, bindingName, isAttribute)
.filter(context => context !== core.SecurityContext.NONE);
let sanitizerFn: o.ExternalExpr|null = null;
if (securityContexts.length) {
if (securityContexts.length === 2 &&
securityContexts.indexOf(core.SecurityContext.URL) > -1 &&
securityContexts.indexOf(core.SecurityContext.RESOURCE_URL) > -1) {
// Special case for some URL attributes (such as "src" and "href") that may be a part
// of different security contexts. In this case we use special santitization function and
// select the actual sanitizer at runtime based on a tag name that is provided while
// invoking sanitization function.
sanitizerFn = o.importExpr(R3.sanitizeUrlOrResourceUrl);
} else {
sanitizerFn = resolveSanitizationFn(securityContexts[0], isAttribute);
}
}
const instructionParams = [o.literal(bindingName), bindingExpr.currValExpr];
if (sanitizerFn) {
instructionParams.push(sanitizerFn);
}
updateStatements.push(...bindingExpr.stmts);
if (instruction === R3.hostProperty) {
propertyBindings.push(instructionParams);
} else if (instruction === R3.attribute) {
attributeBindings.push(instructionParams);
} else if (instruction === R3.updateSyntheticHostBinding) {
syntheticHostBindings.push(instructionParams);
let sanitizerFn: o.ExternalExpr|null = null;
if (securityContexts.length) {
if (securityContexts.length === 2 &&
securityContexts.indexOf(core.SecurityContext.URL) > -1 &&
securityContexts.indexOf(core.SecurityContext.RESOURCE_URL) > -1) {
// Special case for some URL attributes (such as "src" and "href") that may be a part
// of different security contexts. In this case we use special santitization function and
// select the actual sanitizer at runtime based on a tag name that is provided while
// invoking sanitization function.
sanitizerFn = o.importExpr(R3.sanitizeUrlOrResourceUrl);
} else {
updateStatements.push(o.importExpr(instruction).callFn(instructionParams).toStmt());
sanitizerFn = resolveSanitizationFn(securityContexts[0], isAttribute);
}
}
const instructionParams = [o.literal(bindingName), bindingExpr.currValExpr];
if (sanitizerFn) {
instructionParams.push(sanitizerFn);
}
updateStatements.push(...bindingExpr.stmts);
if (instruction === R3.hostProperty) {
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) {
@ -664,7 +674,8 @@ function createHostBindingsFunction(
instruction.calls.forEach(call => {
// we subtract a value of `1` here because the binding slot was already allocated
// at the top of this method when all the input bindings were counted.
totalHostVarsCount += Math.max(call.allocateBindingSlots - 1, 0);
totalHostVarsCount +=
Math.max(call.allocateBindingSlots - MIN_STYLING_BINDING_SLOTS_REQUIRED, 0);
calls.push(convertStylingCall(call, bindingContext, bindingFn));
});

View File

@ -20,6 +20,56 @@ import {DefinitionMap, getInterpolationArgsLength} from './util';
const IMPORTANT_FLAG = '!important';
/**
* Minimum amount of binding slots required in the runtime for style/class bindings.
*
* Styling in Angular uses up two slots in the runtime LView/TData data structures to
* record binding data, property information and metadata.
*
* When a binding is registered it will place the following information in the `LView`:
*
* slot 1) binding value
* slot 2) cached value (all other values collected before it in string form)
*
* When a binding is registered it will place the following information in the `TData`:
*
* slot 1) prop name
* slot 2) binding index that points to the previous style/class binding (and some extra config
* values)
*
* Let's imagine we have a binding that looks like so:
*
* ```
* <div [style.width]="x" [style.height]="y">
* ```
*
* Our `LView` and `TData` data-structures look like so:
*
* ```typescript
* LView = [
* // ...
* x, // value of x
* "width: x",
*
* y, // value of y
* "width: x; height: y",
* // ...
* ];
*
* TData = [
* // ...
* "width", // binding slot 20
* 0,
*
* "height",
* 20,
* // ...
* ];
* ```
*
* */
export const MIN_STYLING_BINDING_SLOTS_REQUIRED = 2;
/**
* A styling expression summary that is to be processed by the compiler
*/
@ -44,6 +94,7 @@ interface BoundStylingEntry {
name: string|null;
unit: string|null;
sourceSpan: ParseSourceSpan;
sanitize: boolean;
value: AST;
}
@ -116,10 +167,6 @@ export class StylingBuilder {
private _initialStyleValues: string[] = [];
private _initialClassValues: string[] = [];
// certain style properties ALWAYS need sanitization
// this is checked each time new styles are encountered
private _useDefaultSanitizer = false;
constructor(private _elementIndexExpr: o.Expression, private _directiveExpr: o.Expression|null) {}
/**
@ -179,14 +226,13 @@ export class StylingBuilder {
const {property, hasOverrideFlag, unit: bindingUnit} = parseProperty(name);
const entry: BoundStylingEntry = {
name: property,
sanitize: property ? isStyleSanitizable(property) : true,
unit: unit || bindingUnit, value, sourceSpan, hasOverrideFlag
};
if (isMapBased) {
this._useDefaultSanitizer = true;
this._styleMapInput = entry;
} else {
(this._singleStyleInputs = this._singleStyleInputs || []).push(entry);
this._useDefaultSanitizer = this._useDefaultSanitizer || isStyleSanitizable(name);
registerIntoMap(this._stylesIndex, property);
}
this._lastStylingInput = entry;
@ -202,8 +248,8 @@ export class StylingBuilder {
return null;
}
const {property, hasOverrideFlag} = parseProperty(name);
const entry:
BoundStylingEntry = {name: property, value, sourceSpan, hasOverrideFlag, unit: null};
const entry: BoundStylingEntry =
{name: property, value, sourceSpan, sanitize: false, hasOverrideFlag, unit: null};
if (isMapBased) {
if (this._classMapInput) {
throw new Error(
@ -319,7 +365,7 @@ export class StylingBuilder {
// map-based bindings allocate two slots: one for the
// previous binding value and another for the previous
// className or style attribute value.
let totalBindingSlotsRequired = 2;
let totalBindingSlotsRequired = MIN_STYLING_BINDING_SLOTS_REQUIRED;
// these values must be outside of the update block so that they can
// be evaluated (the AST visit call) during creation time so that any
@ -341,17 +387,23 @@ export class StylingBuilder {
allocateBindingSlots: totalBindingSlotsRequired,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
const convertResult = convertFn(mapValue);
return Array.isArray(convertResult) ? convertResult : [convertResult];
const params = Array.isArray(convertResult) ? convertResult : [convertResult];
// [style] instructions will sanitize all their values. For this reason we
// need to include the sanitizer as a param.
if (!isClassBased) {
params.push(o.importExpr(R3.defaultStyleSanitizer));
}
return params;
}
}]
};
}
private _buildSingleInputs(
reference: o.ExternalReference, inputs: BoundStylingEntry[], mapIndex: Map<string, number>,
allowUnits: boolean, valueConverter: ValueConverter,
getInterpolationExpressionFn?: (value: Interpolation) => o.ExternalReference):
StylingInstruction[] {
reference: o.ExternalReference, inputs: BoundStylingEntry[], valueConverter: ValueConverter,
getInterpolationExpressionFn: ((value: Interpolation) => o.ExternalReference)|null,
isClassBased: boolean): StylingInstruction[] {
const instructions: StylingInstruction[] = [];
inputs.forEach(input => {
@ -359,7 +411,14 @@ export class StylingBuilder {
instructions[instructions.length - 1];
const value = input.value.visit(valueConverter);
let referenceForCall = reference;
let totalBindingSlotsRequired = 1; // each styling binding value is stored in the LView
// each styling binding value is stored in the LView
// but there are two values stored for each binding:
// 1) the value itself
// 2) an intermediate value (concatenation of style up to this point).
// We need to store the intermediate value so that we don't allocate
// the strings on each CD.
let totalBindingSlotsRequired = MIN_STYLING_BINDING_SLOTS_REQUIRED;
if (value instanceof Interpolation) {
totalBindingSlotsRequired += value.expressions.length;
@ -374,7 +433,7 @@ export class StylingBuilder {
allocateBindingSlots: totalBindingSlotsRequired,
supportsInterpolation: !!getInterpolationExpressionFn,
params: (convertFn: (value: any) => o.Expression | o.Expression[]) => {
// params => stylingProp(propName, value)
// params => stylingProp(propName, value, suffix|sanitizer)
const params: o.Expression[] = [];
params.push(o.literal(input.name));
@ -385,8 +444,16 @@ export class StylingBuilder {
params.push(convertResult);
}
if (allowUnits && input.unit) {
params.push(o.literal(input.unit));
// [style.prop] bindings may use suffix values (e.g. px, em, etc...) and they
// can also use a sanitizer. Sanitization occurs for url-based entries. Having
// the suffix value and a sanitizer together into the instruction doesn't make
// any sense (url-based entries cannot be sanitized).
if (!isClassBased) {
if (input.unit) {
params.push(o.literal(input.unit));
} else if (input.sanitize) {
params.push(o.importExpr(R3.defaultStyleSanitizer));
}
}
return params;
@ -411,7 +478,7 @@ export class StylingBuilder {
private _buildClassInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleClassInputs) {
return this._buildSingleInputs(
R3.classProp, this._singleClassInputs, this._classesIndex, false, valueConverter);
R3.classProp, this._singleClassInputs, valueConverter, null, true);
}
return [];
}
@ -419,23 +486,12 @@ export class StylingBuilder {
private _buildStyleInputs(valueConverter: ValueConverter): StylingInstruction[] {
if (this._singleStyleInputs) {
return this._buildSingleInputs(
R3.styleProp, this._singleStyleInputs, this._stylesIndex, true, valueConverter,
getStylePropInterpolationExpression);
R3.styleProp, this._singleStyleInputs, valueConverter,
getStylePropInterpolationExpression, false);
}
return [];
}
private _buildSanitizerFn(): StylingInstruction {
return {
reference: R3.styleSanitizer,
calls: [{
sourceSpan: this._firstStylingInput ? this._firstStylingInput.sourceSpan : null,
allocateBindingSlots: 0,
params: () => [o.importExpr(R3.defaultStyleSanitizer)]
}]
};
}
/**
* Constructs all instructions which contain the expressions that will be placed
* into the update block of a template function or a directive hostBindings function.
@ -443,9 +499,6 @@ export class StylingBuilder {
buildUpdateLevelInstructions(valueConverter: ValueConverter) {
const instructions: StylingInstruction[] = [];
if (this.hasBindings) {
if (this._useDefaultSanitizer) {
instructions.push(this._buildSanitizerFn());
}
const styleMapInstruction = this.buildStyleMapInstruction(valueConverter);
if (styleMapInstruction) {
instructions.push(styleMapInstruction);

View File

@ -684,7 +684,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
// the code here will collect all update-level styling instructions and add them to the
// update block of the template function AOT code. Instructions like `styleProp`,
// `styleMap`, `classMap`, `classProp` and `stylingApply`
// `styleMap`, `classMap`, `classProp` and `flushStyling`
// are all generated and assigned in the code below.
const stylingInstructions = stylingBuilder.buildUpdateLevelInstructions(this._valueConverter);
const limit = stylingInstructions.length - 1;