perf(ivy): apply [style]/[class] bindings directly to style/className (#33336)
This patch ensures that the `[style]` and `[class]` based bindings are directly applied to an element's style and className attributes. This patch optimizes the algorithm so that it... - Doesn't construct an update an instance of `StylingMapArray` for `[style]` and `[class]` bindings - Doesn't apply `[style]` and `[class]` based entries using `classList` and `style` (direct attributes are used instead) - Doesn't split or iterate over all string-based tokens in a string value obtained from a `[class]` binding. This patch speeds up the following cases: - `<div [class]>` and `<div class="..." [class]>` - `<div [style]>` and `<div style="..." [style]>` The overall speec increase is by over 5x. PR Close #33336
This commit is contained in:

committed by
Andrew Kushnir

parent
ee4fc12e42
commit
dcdb433b7d
@ -7,14 +7,15 @@
|
||||
*/
|
||||
import {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
|
||||
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
|
||||
import {global} from '../../util/global';
|
||||
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
|
||||
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
|
||||
import {NO_CHANGE} from '../tokens';
|
||||
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingValueDefined, lockContext, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
|
||||
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, lockContext, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
|
||||
|
||||
import {getStylingState, resetStylingState} from './state';
|
||||
|
||||
|
||||
const VALUE_IS_EXTERNALLY_MODIFIED = {};
|
||||
|
||||
/**
|
||||
* --------
|
||||
@ -655,44 +656,99 @@ export function applyStylingViaContext(
|
||||
*/
|
||||
export function applyStylingMapDirectly(
|
||||
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
|
||||
bindingIndex: number, map: StylingMapArray, isClassBased: boolean, applyFn: ApplyStylingFn,
|
||||
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
|
||||
if (forceUpdate || hasValueChanged(data[bindingIndex], map)) {
|
||||
setValue(data, bindingIndex, map);
|
||||
const initialStyles =
|
||||
hasConfig(context, TStylingConfig.HasInitialStyling) ? getStylingMapArray(context) : null;
|
||||
bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean,
|
||||
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean,
|
||||
bindingValueContainsInitial?: boolean): void {
|
||||
const oldValue = getValue(data, bindingIndex);
|
||||
if (forceUpdate || hasValueChanged(oldValue, value)) {
|
||||
const config = getConfig(context);
|
||||
const hasInitial = config & TStylingConfig.HasInitialStyling;
|
||||
const initialValue =
|
||||
hasInitial && !bindingValueContainsInitial ? getInitialStylingValue(context) : null;
|
||||
setValue(data, bindingIndex, value);
|
||||
|
||||
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
|
||||
i += StylingMapArrayIndex.TupleSize) {
|
||||
const prop = getMapProp(map, i);
|
||||
const value = getMapValue(map, i);
|
||||
// the cached value is the last snapshot of the style or class
|
||||
// attribute value and is used in the if statement below to
|
||||
// keep track of internal/external changes.
|
||||
const cachedValueIndex = bindingIndex + 1;
|
||||
let cachedValue = getValue(data, cachedValueIndex);
|
||||
if (cachedValue === NO_CHANGE) {
|
||||
cachedValue = initialValue;
|
||||
}
|
||||
cachedValue = typeof cachedValue !== 'string' ? '' : cachedValue;
|
||||
|
||||
// case 1: apply the map value (if it exists)
|
||||
let applied =
|
||||
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
|
||||
// If a class/style value was modified externally then the styling
|
||||
// fast pass cannot guarantee that the external values are retained.
|
||||
// When this happens, the algorithm will bail out and not write to
|
||||
// the style or className attribute directly.
|
||||
let writeToAttrDirectly = !(config & TStylingConfig.HasPropBindings);
|
||||
if (writeToAttrDirectly &&
|
||||
checkIfExternallyModified(element as HTMLElement, cachedValue, isClassBased)) {
|
||||
writeToAttrDirectly = false;
|
||||
if (oldValue !== VALUE_IS_EXTERNALLY_MODIFIED) {
|
||||
// direct styling will reset the attribute entirely each time,
|
||||
// and, for this reason, if the algorithm decides it cannot
|
||||
// write to the class/style attributes directly then it must
|
||||
// reset all the previous style/class values before it starts
|
||||
// to apply values in the non-direct way.
|
||||
removeStylingValues(renderer, element, oldValue, isClassBased);
|
||||
|
||||
// case 2: apply the initial value (if it exists)
|
||||
if (!applied && initialStyles) {
|
||||
applied = findAndApplyMapValue(
|
||||
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
|
||||
}
|
||||
|
||||
// default case: apply `null` to remove the value
|
||||
if (!applied) {
|
||||
applyFn(renderer, element, prop, null, bindingIndex);
|
||||
// this will instruct the algorithm not to apply class or style
|
||||
// values directly anymore.
|
||||
setValue(data, cachedValueIndex, VALUE_IS_EXTERNALLY_MODIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
|
||||
if (isClassBased) {
|
||||
state.lastDirectClassMap = map;
|
||||
if (writeToAttrDirectly) {
|
||||
let valueToApply: string;
|
||||
if (isClassBased) {
|
||||
valueToApply = typeof value === 'string' ? value : objectToClassName(value);
|
||||
if (initialValue !== null) {
|
||||
valueToApply = concatString(initialValue, valueToApply, ' ');
|
||||
}
|
||||
setClassName(renderer, element, valueToApply);
|
||||
} else {
|
||||
valueToApply = forceStylesAsString(value as{[key: string]: any}, true);
|
||||
if (initialValue !== null) {
|
||||
valueToApply = initialValue + ';' + valueToApply;
|
||||
}
|
||||
setStyleAttr(renderer, element, valueToApply);
|
||||
}
|
||||
setValue(data, cachedValueIndex, valueToApply || null);
|
||||
} else {
|
||||
state.lastDirectStyleMap = map;
|
||||
}
|
||||
const applyFn = isClassBased ? setClass : setStyle;
|
||||
const map = normalizeIntoStylingMap(oldValue, value, !isClassBased);
|
||||
const initialStyles = hasInitial ? getStylingMapArray(context) : null;
|
||||
|
||||
return true;
|
||||
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
|
||||
i += StylingMapArrayIndex.TupleSize) {
|
||||
const prop = getMapProp(map, i);
|
||||
const value = getMapValue(map, i);
|
||||
|
||||
// case 1: apply the map value (if it exists)
|
||||
let applied =
|
||||
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
|
||||
|
||||
// case 2: apply the initial value (if it exists)
|
||||
if (!applied && initialStyles) {
|
||||
applied = findAndApplyMapValue(
|
||||
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
|
||||
}
|
||||
|
||||
// default case: apply `null` to remove the value
|
||||
if (!applied) {
|
||||
applyFn(renderer, element, prop, null, bindingIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
|
||||
if (isClassBased) {
|
||||
state.lastDirectClassMap = map;
|
||||
} else {
|
||||
state.lastDirectStyleMap = map;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -727,11 +783,12 @@ export function applyStylingMapDirectly(
|
||||
*/
|
||||
export function applyStylingValueDirectly(
|
||||
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
|
||||
bindingIndex: number, prop: string, value: any, isClassBased: boolean, applyFn: ApplyStylingFn,
|
||||
bindingIndex: number, prop: string, value: any, isClassBased: boolean,
|
||||
sanitizer?: StyleSanitizeFn | null): boolean {
|
||||
let applied = false;
|
||||
if (hasValueChanged(data[bindingIndex], value)) {
|
||||
setValue(data, bindingIndex, value);
|
||||
const applyFn = isClassBased ? setClass : setStyle;
|
||||
|
||||
// case 1: apply the provided value (if it exists)
|
||||
applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
|
||||
@ -888,6 +945,26 @@ export const setClass: ApplyStylingFn =
|
||||
}
|
||||
};
|
||||
|
||||
export const setClassName = (renderer: Renderer3 | null, native: RElement, className: string) => {
|
||||
if (renderer !== null) {
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
renderer.setAttribute(native, 'class', className);
|
||||
} else {
|
||||
native.className = className;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setStyleAttr = (renderer: Renderer3 | null, native: RElement, value: string) => {
|
||||
if (renderer !== null) {
|
||||
if (isProceduralRenderer(renderer)) {
|
||||
renderer.setAttribute(native, 'style', value);
|
||||
} else {
|
||||
native.setAttribute('style', value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates over all provided styling entries and renders them on the element.
|
||||
*
|
||||
@ -914,3 +991,63 @@ export function renderStylingMap(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function objectToClassName(obj: {[key: string]: any} | null): string {
|
||||
let str = '';
|
||||
if (obj) {
|
||||
for (let key in obj) {
|
||||
const value = obj[key];
|
||||
if (value) {
|
||||
str += (str.length ? ' ' : '') + key;
|
||||
}
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not an element style/className value has changed since the last update.
|
||||
*
|
||||
* This function helps Angular determine if a style or class attribute value was
|
||||
* modified by an external plugin or API outside of the style binding code. This
|
||||
* means any JS code that adds/removes class/style values on an element outside
|
||||
* of Angular's styling binding algorithm.
|
||||
*
|
||||
* @returns true when the value was modified externally.
|
||||
*/
|
||||
function checkIfExternallyModified(element: HTMLElement, cachedValue: any, isClassBased: boolean) {
|
||||
// this means it was checked before and there is no reason
|
||||
// to compare the style/class values again. Either that or
|
||||
// web workers are being used.
|
||||
if (global.Node === 'undefined' || cachedValue === VALUE_IS_EXTERNALLY_MODIFIED) return true;
|
||||
|
||||
// comparing the DOM value against the cached value is the best way to
|
||||
// see if something has changed.
|
||||
const currentValue =
|
||||
(isClassBased ? element.className : (element.style && element.style.cssText)) || '';
|
||||
return currentValue !== (cachedValue || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes provided styling values from the element
|
||||
*/
|
||||
function removeStylingValues(
|
||||
renderer: any, element: RElement, values: string | {[key: string]: any} | StylingMapArray,
|
||||
isClassBased: boolean) {
|
||||
let arr: StylingMapArray;
|
||||
if (isStylingMapArray(values)) {
|
||||
arr = values as StylingMapArray;
|
||||
} else {
|
||||
arr = normalizeIntoStylingMap(null, values, !isClassBased);
|
||||
}
|
||||
|
||||
const applyFn = isClassBased ? setClass : setStyle;
|
||||
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < arr.length;
|
||||
i += StylingMapArrayIndex.TupleSize) {
|
||||
const value = getMapValue(arr, i);
|
||||
if (value) {
|
||||
const prop = getMapProp(arr, i);
|
||||
applyFn(renderer, element, prop, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,13 @@
|
||||
* 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 {createProxy} from '../../debug/proxy';
|
||||
import {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
|
||||
import {RElement} from '../interfaces/renderer';
|
||||
import {ApplyStylingFn, LStylingData, TStylingConfig, TStylingContext, TStylingContextIndex} from '../interfaces/styling';
|
||||
import {getCurrentStyleSanitizer} from '../state';
|
||||
import {attachDebugObject} from '../util/debug_utils';
|
||||
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext} from '../util/styling_utils';
|
||||
import {MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, allowDirectStyling as _allowDirectStyling, getBindingValue, getDefaultValue, getGuardMask, getProp, getPropValuesStartPosition, getValue, getValuesCount, hasConfig, isContextLocked, isSanitizationRequired, isStylingContext, normalizeIntoStylingMap, setValue} from '../util/styling_utils';
|
||||
|
||||
import {applyStylingViaContext} from './bindings';
|
||||
import {activateStylingMapFeature} from './map_based_bindings';
|
||||
@ -374,10 +375,52 @@ export class NodeStylingDebug implements DebugNodeStyling {
|
||||
*/
|
||||
get summary(): {[key: string]: DebugNodeStylingEntry} {
|
||||
const entries: {[key: string]: DebugNodeStylingEntry} = {};
|
||||
this._mapValues((prop: string, value: any, bindingIndex: number | null) => {
|
||||
const config = this.config;
|
||||
const isClassBased = this._isClassBased;
|
||||
|
||||
let data = this._data;
|
||||
|
||||
// the direct pass code doesn't convert [style] or [class] values
|
||||
// into StylingMapArray instances. For this reason, the values
|
||||
// need to be converted ahead of time since the styling debug
|
||||
// relies on context resolution to figure out what styling
|
||||
// values have been added/removed on the element.
|
||||
if (config.allowDirectStyling && config.hasMapBindings) {
|
||||
data = data.concat([]); // make a copy
|
||||
this._convertMapBindingsToStylingMapArrays(data);
|
||||
}
|
||||
|
||||
this._mapValues(data, (prop: string, value: any, bindingIndex: number | null) => {
|
||||
entries[prop] = {prop, value, bindingIndex};
|
||||
});
|
||||
return entries;
|
||||
|
||||
// because the styling algorithm runs into two different
|
||||
// modes: direct and context-resolution, the output of the entries
|
||||
// object is different because the removed values are not
|
||||
// saved between updates. For this reason a proxy is created
|
||||
// so that the behavior is the same when examining values
|
||||
// that are no longer active on the element.
|
||||
return createProxy({
|
||||
get(target: {}, prop: string): DebugNodeStylingEntry{
|
||||
let value: DebugNodeStylingEntry = entries[prop]; if (!value) {
|
||||
value = {
|
||||
prop,
|
||||
value: isClassBased ? false : null,
|
||||
bindingIndex: null,
|
||||
};
|
||||
} return value;
|
||||
},
|
||||
set(target: {}, prop: string, value: any) { return false; },
|
||||
ownKeys() { return Object.keys(entries); },
|
||||
getOwnPropertyDescriptor(k: any) {
|
||||
// we use a special property descriptor here so that enumeration operations
|
||||
// such as `Object.keys` will work on this proxy.
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get config() { return buildConfig(this.context.context); }
|
||||
@ -387,11 +430,41 @@ export class NodeStylingDebug implements DebugNodeStyling {
|
||||
*/
|
||||
get values(): {[key: string]: any} {
|
||||
const entries: {[key: string]: any} = {};
|
||||
this._mapValues((prop: string, value: any) => { entries[prop] = value; });
|
||||
const config = this.config;
|
||||
let data = this._data;
|
||||
|
||||
// the direct pass code doesn't convert [style] or [class] values
|
||||
// into StylingMapArray instances. For this reason, the values
|
||||
// need to be converted ahead of time since the styling debug
|
||||
// relies on context resolution to figure out what styling
|
||||
// values have been added/removed on the element.
|
||||
if (config.allowDirectStyling && config.hasMapBindings) {
|
||||
data = data.concat([]); // make a copy
|
||||
this._convertMapBindingsToStylingMapArrays(data);
|
||||
}
|
||||
|
||||
this._mapValues(data, (prop: string, value: any) => { entries[prop] = value; });
|
||||
return entries;
|
||||
}
|
||||
|
||||
private _mapValues(fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
|
||||
private _convertMapBindingsToStylingMapArrays(data: LStylingData) {
|
||||
const context = this.context.context;
|
||||
const limit = getPropValuesStartPosition(context);
|
||||
for (let i =
|
||||
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
|
||||
i < limit; i++) {
|
||||
const bindingIndex = context[i] as number;
|
||||
const bindingValue = bindingIndex !== 0 ? getValue(data, bindingIndex) : null;
|
||||
if (bindingValue && !Array.isArray(bindingValue)) {
|
||||
const stylingMapArray = normalizeIntoStylingMap(null, bindingValue, !this._isClassBased);
|
||||
setValue(data, bindingIndex, stylingMapArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _mapValues(
|
||||
data: LStylingData,
|
||||
fn: (prop: string, value: string|null, bindingIndex: number|null) => any) {
|
||||
// there is no need to store/track an element instance. The
|
||||
// element is only used when the styling algorithm attempts to
|
||||
// style the value (and we mock out the stylingApplyFn anyway).
|
||||
@ -409,11 +482,11 @@ export class NodeStylingDebug implements DebugNodeStyling {
|
||||
|
||||
// run the template bindings
|
||||
applyStylingViaContext(
|
||||
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, false);
|
||||
this.context.context, null, mockElement, data, true, mapFn, sanitizer, false);
|
||||
|
||||
// and also the host bindings
|
||||
applyStylingViaContext(
|
||||
this.context.context, null, mockElement, this._data, true, mapFn, sanitizer, true);
|
||||
this.context.context, null, mockElement, data, true, mapFn, sanitizer, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user