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:

committed by
Miško Hevery

parent
b7ff38b1ef
commit
4005815114
@ -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};
|
||||
|
@ -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));
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user