perf(ivy): initialise TNode inputs / outputs on the first creation pass (#32608)

This perf-focused refactoring moves the TNode's input / output initialization
logic to the first template pass - close to the place where directives are
matched and resolved.

This code change makes it possible to update-mode checks for both property
bindings and listeners registration.

PR Close #32608
This commit is contained in:
Pawel Kozlowski 2019-09-11 14:48:15 +02:00 committed by Kara Erickson
parent adeee0fa7f
commit ad178c55fd
5 changed files with 71 additions and 88 deletions

View File

@ -23,7 +23,7 @@ import {getInitialStylingValue, hasClassInput, hasStyleInput} from '../styling_n
import {setUpAttributes} from '../util/attrs_utils'; import {setUpAttributes} from '../util/attrs_utils';
import {getNativeByTNode, getTNode} from '../util/view_utils'; import {getNativeByTNode, getTNode} from '../util/view_utils';
import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, initializeTNodeInputs, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared'; import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared';
@ -84,13 +84,14 @@ export function ɵɵelementStart(
ngDevMode && ngDevMode.firstTemplatePass++; ngDevMode && ngDevMode.firstTemplatePass++;
resolveDirectives(tView, lView, tNode, localRefs || null); resolveDirectives(tView, lView, tNode, localRefs || null);
const inputData = initializeTNodeInputs(tView, tNode); const inputData = tNode.inputs;
if (inputData && inputData.hasOwnProperty('class')) { if (inputData != null) {
tNode.flags |= TNodeFlags.hasClassInput; if (inputData.hasOwnProperty('class')) {
} tNode.flags |= TNodeFlags.hasClassInput;
}
if (inputData && inputData.hasOwnProperty('style')) { if (inputData.hasOwnProperty('style')) {
tNode.flags |= TNodeFlags.hasStyleInput; tNode.flags |= TNodeFlags.hasStyleInput;
}
} }
if (tView.queries !== null) { if (tView.queries !== null) {

View File

@ -17,8 +17,7 @@ import {CLEANUP, FLAGS, LView, LViewFlags, RENDERER, TVIEW} from '../interfaces/
import {assertNodeOfPossibleTypes} from '../node_assert'; import {assertNodeOfPossibleTypes} from '../node_assert';
import {getLView, getPreviousOrParentTNode} from '../state'; import {getLView, getPreviousOrParentTNode} from '../state';
import {getComponentViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils'; import {getComponentViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
import {getCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
import {BindingDirection, generatePropertyAliases, getCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
/** /**
* Adds an event listener to the current node. * Adds an event listener to the current node.
@ -186,36 +185,28 @@ function listenerInternal(
} }
// subscribe to directive outputs // subscribe to directive outputs
if (isTNodeDirectiveHost && processOutputs) { const outputs = tNode.outputs;
let outputs = tNode.outputs; let props: PropertyAliasValue|undefined;
if (outputs === undefined) { if (processOutputs && outputs != null && (props = outputs[eventName])) {
// if we create TNode here, inputs must be undefined so we know they still need to be const propsLength = props.length;
// checked if (propsLength) {
outputs = tNode.outputs = generatePropertyAliases(tView, tNode, BindingDirection.Output); const lCleanup = getCleanup(lView);
} for (let i = 0; i < propsLength; i += 3) {
const index = props[i] as number;
ngDevMode && assertDataInRange(lView, index);
const minifiedName = props[i + 2];
const directiveInstance = lView[index];
const output = directiveInstance[minifiedName];
let props: PropertyAliasValue|undefined; if (ngDevMode && !isObservable(output)) {
if (outputs !== null && (props = outputs[eventName])) { throw new Error(
const propsLength = props.length; `@Output ${minifiedName} not initialized in '${directiveInstance.constructor.name}'.`);
if (propsLength) {
const lCleanup = getCleanup(lView);
for (let i = 0; i < propsLength; i += 3) {
const index = props[i] as number;
ngDevMode && assertDataInRange(lView, index);
const minifiedName = props[i + 2];
const directiveInstance = lView[index];
const output = directiveInstance[minifiedName];
if (ngDevMode && !isObservable(output)) {
throw new Error(
`@Output ${minifiedName} not initialized in '${directiveInstance.constructor.name}'.`);
}
const subscription = output.subscribe(listenerFn);
const idx = lCleanup.length;
lCleanup.push(listenerFn, subscription);
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
} }
const subscription = output.subscribe(listenerFn);
const idx = lCleanup.length;
lCleanup.push(listenerFn, subscription);
tCleanup && tCleanup.push(eventName, tNode.index, idx, -(idx + 1));
} }
} }
} }

View File

@ -21,7 +21,7 @@ import {diPublicInInjector, getNodeInjectable, getOrCreateNodeInjectorForNode} f
import {throwMultipleComponentError} from '../errors'; import {throwMultipleComponentError} from '../errors';
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPreOrderHooks} from '../hooks'; import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags, registerPreOrderHooks} from '../hooks';
import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container'; import {ACTIVE_INDEX, CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, FactoryFn, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; import {ComponentDef, ComponentTemplate, DirectiveDef, DirectiveDefListOrFactory, PipeDefListOrFactory, RenderFlags, ViewQueriesFunction} from '../interfaces/definition';
import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector'; import {INJECTOR_BLOOM_PARENT_SIZE, NodeInjectorFactory} from '../interfaces/injector';
import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node'; import {AttributeMarker, InitialInputData, InitialInputs, LocalRefExtractor, PropertyAliasValue, PropertyAliases, TAttributes, TContainerNode, TElementContainerNode, TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TProjectionNode, TViewNode} from '../interfaces/node';
import {RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer'; import {RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from '../interfaces/renderer';
@ -49,11 +49,6 @@ import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNod
*/ */
const _CLEAN_PROMISE = (() => Promise.resolve(null))(); const _CLEAN_PROMISE = (() => Promise.resolve(null))();
export const enum BindingDirection {
Input,
Output,
}
/** Sets the host bindings for the current view. */ /** Sets the host bindings for the current view. */
export function setHostBindings(tView: TView, viewData: LView): void { export function setHostBindings(tView: TView, viewData: LView): void {
const selectedIndex = getSelectedIndex(); const selectedIndex = getSelectedIndex();
@ -803,41 +798,47 @@ export function createTNode(
} }
/** function generatePropertyAliases(
* Consolidates all inputs or outputs of all directives on this logical node. inputAliasMap: {[publicName: string]: string}, directiveDefIdx: number,
* propStore: PropertyAliases | null): PropertyAliases|null {
* @param tNode for (let publicName in inputAliasMap) {
* @param direction whether to consider inputs or outputs if (inputAliasMap.hasOwnProperty(publicName)) {
* @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise propStore = propStore === null ? {} : propStore;
*/ const internalName = inputAliasMap[publicName];
export function generatePropertyAliases(
tView: TView, tNode: TNode, direction: BindingDirection): PropertyAliases|null {
let propStore: PropertyAliases|null = null;
const start = tNode.directiveStart;
const end = tNode.directiveEnd;
if (end > start) { if (propStore.hasOwnProperty(publicName)) {
const isInput = direction === BindingDirection.Input; propStore[publicName].push(directiveDefIdx, publicName, internalName);
const defs = tView.data; } else {
(propStore[publicName] = [directiveDefIdx, publicName, internalName]);
for (let i = start; i < end; i++) {
const directiveDef = defs[i] as DirectiveDef<any>;
const propertyAliasMap: {[publicName: string]: string} =
isInput ? directiveDef.inputs : directiveDef.outputs;
for (let publicName in propertyAliasMap) {
if (propertyAliasMap.hasOwnProperty(publicName)) {
propStore = propStore || {};
const internalName = propertyAliasMap[publicName];
const hasProperty = propStore.hasOwnProperty(publicName);
hasProperty ? propStore[publicName].push(i, publicName, internalName) :
(propStore[publicName] = [i, publicName, internalName]);
}
} }
} }
} }
return propStore; return propStore;
} }
/**
* Initializes data structures required to work with directive outputs and outputs.
* Initialization is done for all directives matched on a given TNode.
*/
function initializeInputAndOutputAliases(tView: TView, tNode: TNode): void {
ngDevMode && assertFirstTemplatePass(tView);
const start = tNode.directiveStart;
const end = tNode.directiveEnd;
const defs = tView.data;
let inputsStore: PropertyAliases|null = null;
let outputsStore: PropertyAliases|null = null;
for (let i = start; i < end; i++) {
const directiveDef = defs[i] as DirectiveDef<any>;
inputsStore = generatePropertyAliases(directiveDef.inputs, i, inputsStore);
outputsStore = generatePropertyAliases(directiveDef.outputs, i, outputsStore);
}
tNode.inputs = inputsStore;
tNode.outputs = outputsStore;
}
/** /**
* Mapping between attributes names that don't correspond to their element property names. * Mapping between attributes names that don't correspond to their element property names.
* *
@ -863,13 +864,11 @@ export function elementPropertyInternal<T>(
loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void { loadRendererFn?: ((tNode: TNode, lView: LView) => Renderer3) | null): void {
ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.'); ngDevMode && assertNotSame(value, NO_CHANGE as any, 'Incoming value should never be NO_CHANGE.');
const lView = getLView(); const lView = getLView();
const tView = lView[TVIEW];
const element = getNativeByIndex(index, lView) as RElement | RComment; const element = getNativeByIndex(index, lView) as RElement | RComment;
const tNode = getTNode(index, lView); const tNode = getTNode(index, lView);
let inputData: PropertyAliases|null|undefined; let inputData = tNode.inputs;
let dataValue: PropertyAliasValue|undefined; let dataValue: PropertyAliasValue|undefined;
if (!nativeOnly && (inputData = initializeTNodeInputs(tView, tNode)) && if (!nativeOnly && inputData != null && (dataValue = inputData[propName])) {
(dataValue = inputData[propName])) {
setInputsForProperty(lView, dataValue, value); setInputsForProperty(lView, dataValue, value);
if (isComponentHost(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET); if (isComponentHost(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
if (ngDevMode) { if (ngDevMode) {
@ -1055,6 +1054,8 @@ export function resolveDirectives(
directiveDefIdx, def, tView, nodeIndex, initialPreOrderHooksLength, directiveDefIdx, def, tView, nodeIndex, initialPreOrderHooksLength,
initialPreOrderCheckHooksLength); initialPreOrderCheckHooksLength);
} }
initializeInputAndOutputAliases(tView, tNode);
} }
if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap); if (exportsMap) cacheMatchingLocalNames(tNode, localRefs, exportsMap);
} }
@ -1773,16 +1774,6 @@ export function storePropertyBindingMetadata(
export const CLEAN_PROMISE = _CLEAN_PROMISE; export const CLEAN_PROMISE = _CLEAN_PROMISE;
export function initializeTNodeInputs(tView: TView, tNode: TNode): PropertyAliases|null {
// If tNode.inputs is undefined, a listener has created outputs, but inputs haven't
// yet been checked.
if (tNode.inputs === undefined) {
// mark inputs as checked
tNode.inputs = generatePropertyAliases(tView, tNode, BindingDirection.Input);
}
return tNode.inputs;
}
export function getCleanup(view: LView): any[] { export function getCleanup(view: LView): any[] {
// top level variables should not be exported for performance reasons (PERF_NOTES.md) // top level variables should not be exported for performance reasons (PERF_NOTES.md)
return view[CLEANUP] || (view[CLEANUP] = ngDevMode ? new LCleanup() : []); return view[CLEANUP] || (view[CLEANUP] = ngDevMode ? new LCleanup() : []);

View File

@ -423,7 +423,7 @@
"name": "initNodeFlags" "name": "initNodeFlags"
}, },
{ {
"name": "initializeTNodeInputs" "name": "initializeInputAndOutputAliases"
}, },
{ {
"name": "insertBloom" "name": "insertBloom"

View File

@ -936,7 +936,7 @@
"name": "initNodeFlags" "name": "initNodeFlags"
}, },
{ {
"name": "initializeTNodeInputs" "name": "initializeInputAndOutputAliases"
}, },
{ {
"name": "injectElementRef" "name": "injectElementRef"