refactor(core): store directive defs in static data (#20855)

PR Close #20855
This commit is contained in:
Kara Erickson
2017-12-11 14:08:52 -08:00
committed by Igor Minar
parent f3d38ce053
commit 8fdb1e09c1
17 changed files with 399 additions and 386 deletions

View File

@ -143,10 +143,12 @@ export function renderComponent<T>(
const renderer = opts.renderer || document;
const componentDef = componentType.ngComponentDef;
let component: T;
const oldView = enterView(createViewState(-1, renderer), null);
const oldView = enterView(createViewState(-1, renderer, []), null);
try {
elementHost(opts.host || componentDef.tag);
component = directiveCreate(0, componentDef.n(), componentDef);
// Create element node at index 0 in data array
elementHost(opts.host || componentDef.tag, componentDef);
// Create directive instance with n() and store at index 1 in data array (el is 0)
component = directiveCreate(1, componentDef.n(), componentDef);
} finally {
leaveView(oldView);
}
@ -165,7 +167,9 @@ export function detectChanges<T>(component: T) {
ngDevMode && assertNotNull(hostNode.data, 'hostNode.data');
const oldView = enterView(hostNode.view !, hostNode);
try {
(component.constructor as ComponentType<T>).ngComponentDef.r(0, 0);
// Element was stored at 0 and directive was stored at 1 in renderComponent
// so to refresh the component, r() needs to be called with (1, 0)
(component.constructor as ComponentType<T>).ngComponentDef.r(1, 0);
isDirty = false;
} finally {
leaveView(oldView);

View File

@ -9,7 +9,7 @@
import {ComponentFactory, ComponentRef as IComponentRef, ElementRef as IElementRef, EmbeddedViewRef as IEmbeddedViewRef, Injector, NgModuleRef as INgModuleRef, TemplateRef as ITemplateRef, Type, ViewContainerRef as IViewContainerRef, ViewRef as IViewRef} from '../core';
import {BLOOM_SIZE, NG_ELEMENT_ID, getOrCreateNodeInjector} from './instructions';
import {LContainer, LNodeFlags, LNodeInjector} from './interfaces';
import {ComponentTemplate} from './public_interfaces';
import {ComponentTemplate, DirectiveDef} from './public_interfaces';
import {stringify} from './util';
export const enum InjectFlags {
@ -43,13 +43,11 @@ export function inject<T>(token: Type<T>, flags?: InjectFlags): T {
if (size !== 0) {
size = size >> LNodeFlags.SIZE_SHIFT;
const start = flags >> LNodeFlags.INDX_SHIFT;
const directives = node.view.directives;
if (directives) {
for (let i = start, ii = start + size; i < ii; i++) {
const def = directives[(i << 1) | 1];
if (def.diPublic && def.type == token) {
return directives[i << 1];
}
const ngStaticData = node.view.ngStaticData;
for (let i = start, ii = start + size; i < ii; i++) {
const def = ngStaticData[i] as DirectiveDef<any>;
if (def.diPublic && def.type == token) {
return node.view.data[i];
}
}
}

View File

@ -10,7 +10,11 @@ import './ng_dev_mode';
import {Type} from '../core';
import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert';
import {CSSSelector, ContainerState, InitialInputData, InitialInputs, LContainer, LContainerStatic, LElement, LNode, LNodeFlags, LNodeInjector, LNodeStatic, LProjection, LText, LView, MinificationData, MinificationDataValue, ProjectionState, QueryState, ViewState} from './interfaces';
import {
CSSSelector, ContainerState, InitialInputData, InitialInputs, LContainer, LContainerStatic, LElement, LNode,
LNodeFlags, LNodeInjector, LNodeStatic, LProjection, LText, LView, MinificationData, MinificationDataValue,
ProjectionState, QueryState, ViewState, NgStaticData
} from './interfaces';
import {assertNodeType} from './node_assert';
import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation';
import {isNodeMatchingSelector} from './node_selector_matcher';
@ -48,12 +52,12 @@ let isParent: boolean;
* in the data array. Any nodes that do not have static data store a null
* value to avoid a sparse array.
*/
let ngStaticData: (LNodeStatic | null)[];
let ngStaticData: NgStaticData;
/**
* State of the current view being processed.
*/
let currentView: ViewState = createViewState(null !, null !);
let currentView: ViewState = createViewState(null !, null !, []);
let currentQuery: QueryState|null;
@ -68,18 +72,6 @@ let creationMode: boolean;
*/
let data: any[];
/**
* An array of directives in the current view
*
* even indices: contain the directive instance.
* odd indices: contain the directive def
*
* We must store the directive def (rather than token | null)
* because we need to be able to access the inputs and outputs
* of directives that aren't diPublic.
*/
let directives: any[];
/**
* Points to the next binding index to read or write to.
*/
@ -117,9 +109,9 @@ let cleanup: any[]|null;
*/
export function enterView(newViewState: ViewState, host: LElement | LView | null): ViewState {
const oldViewState = currentView;
directives = newViewState.directives;
data = newViewState.data;
bindingIndex = newViewState.bindingStartIndex || 0;
ngStaticData = newViewState.ngStaticData;
if (creationMode = !data) {
// Absence of data implies creationMode.
@ -139,13 +131,13 @@ export function enterView(newViewState: ViewState, host: LElement | LView | null
export const leaveView: (newViewState: ViewState) => void = enterView as any;
export function createViewState(viewId: number, renderer: Renderer3): ViewState {
export function createViewState(viewId: number, renderer: Renderer3, ngStaticData: NgStaticData): ViewState {
const newView = {
parent: currentView,
id: viewId, // -1 for component views
node: null !, // until we initialize it in createNode.
data: null !, // Hack use as a marker for creationMode
directives: [],
ngStaticData: ngStaticData,
cleanup: null,
renderer: renderer,
child: null,
@ -206,10 +198,10 @@ export function createLNode(
data[index] = node;
// Every node adds a value to the static data array to avoid a sparse array
if (ngStaticData && index >= ngStaticData.length) {
if (index >= ngStaticData.length) {
ngStaticData[index] = null;
} else if (ngStaticData) {
node.staticData = ngStaticData[index];
} else {
node.staticData = ngStaticData[index] as LNodeStatic;
}
// Now link ourselves into the tree.
@ -248,9 +240,9 @@ export function createLNode(
export function renderTemplate<T>(host: LElement, template: ComponentTemplate<T>, context: T) {
const hostView = host.data !;
ngDevMode && assertNotEqual(hostView, null, 'hostView');
hostView.ngStaticData = getTemplateStatic(template);
const oldView = enterView(hostView, host);
try {
ngStaticData = template.ngStaticData || (template.ngStaticData = [] as never);
template(context, creationMode);
} finally {
leaveView(oldView);
@ -345,13 +337,19 @@ export function elementCreate(
throw 'for now name is required';
} else {
native = renderer.createElement(name);
let componentView: ViewState | null = null;
if (isHostElement) {
const ngStaticData = getTemplateStatic((nameOrComponentDef as ComponentDef<any>).template);
componentView = addToViewTree(createViewState(-1, renderer, ngStaticData));
}
// Only component views should be added to the view tree directly. Embedded views are
// accessed through their containers because they may be removed / re-added later.
node = createLNode(
index, LNodeFlags.Element, native,
isHostElement ? addToViewTree(createViewState(-1, renderer)) : null);
node = createLNode(index, LNodeFlags.Element, native, componentView);
if (node.staticData == null) {
ngDevMode && assertDataInRange(index - 1);
node.staticData = ngStaticData[index] = createStaticData(name, attrs || null, null);
}
@ -362,6 +360,17 @@ export function elementCreate(
return native;
}
/**
* Gets static data from a template function or creates a new static
* data array if it doesn't already exist.
*
* @param template The template from which to get static data
* @returns NgStaticData
*/
function getTemplateStatic(template: ComponentTemplate<any>): NgStaticData {
return template.ngStaticData || (template.ngStaticData = [] as never);
}
function setUpAttributes(native: RElement, attrs: string[]): void {
ngDevMode && assertEqual(attrs.length % 2, 0, 'attrs.length % 2');
const isFnRenderer = (renderer as Renderer3Fn).setAttribute;
@ -381,7 +390,7 @@ export function createError(text: string, token: any) {
*
* @param elementOrSelector Render element or CSS selector to locate the element.
*/
export function elementHost(elementOrSelector: RElement | string) {
export function elementHost(elementOrSelector: RElement | string, def: ComponentDef<any>) {
ngDevMode && assertDataInRange(-1);
const rNode = typeof elementOrSelector === 'string' ?
((renderer as Renderer3Fn).selectRootElement ?
@ -395,7 +404,7 @@ export function elementHost(elementOrSelector: RElement | string) {
throw createError('Host node is required:', elementOrSelector);
}
}
createLNode(0, LNodeFlags.Element, rNode, createViewState(-1, renderer));
createLNode(0, LNodeFlags.Element, rNode, createViewState(-1, renderer, getTemplateStatic(def.template)));
}
@ -446,9 +455,9 @@ export function listenerCreate(
*/
function outputCreate(outputs: (number | string)[], listener: Function): void {
for (let i = 0; i < outputs.length; i += 2) {
ngDevMode && assertDirectivesInRange((outputs[i] as number) << 1);
ngDevMode && assertDataInRange(outputs[i] as number);
const subscription =
directives[(outputs[i] as number) << 1][outputs[i | 1]].subscribe(listener);
data[outputs[i] as number][outputs[i | 1]].subscribe(listener);
cleanup !.push(subscription.unsubscribe, subscription);
}
}
@ -512,8 +521,7 @@ export function elementProperty<T>(index: number, propName: string, value: T | N
let staticData: LNodeStatic|null = node.staticData !;
// if staticData.inputs is undefined, a listener has created output staticData, but inputs haven't
// yet been
// checked
// yet been checked
if (staticData.inputs === undefined) {
// mark inputs as checked
staticData.inputs = null;
@ -541,7 +549,8 @@ function createStaticData(
attrs,
initialInputs: undefined,
inputs: undefined,
outputs: undefined, containerStatic
outputs: undefined,
containerStatic: containerStatic
};
}
@ -551,8 +560,8 @@ function createStaticData(
*/
function setInputsForProperty(inputs: (number | string)[], value: any): void {
for (let i = 0; i < inputs.length; i += 2) {
ngDevMode && assertDirectivesInRange(inputs[i] as number << 1);
directives[(inputs[i] as number) << 1][inputs[i | 1]] = value;
ngDevMode && assertDataInRange(inputs[i] as number);
data[inputs[i] as number][inputs[i | 1]] = value;
}
}
@ -568,7 +577,7 @@ function generateMinifiedData(flags: number, data: LNodeStatic, isInputData = fa
const size = (flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT;
for (let i = start, ii = start + size; i < ii; i++) {
const directiveDef: DirectiveDef<any> = directives[(i << 1) | 1];
const directiveDef: DirectiveDef<any> = ngStaticData ![i] as DirectiveDef<any>;
const minifiedPropertyMap: {[minifiedKey: string]: string} =
isInputData ? directiveDef.inputs : directiveDef.outputs;
for (let unminifiedKey in minifiedPropertyMap) {
@ -716,11 +725,10 @@ export function directiveCreate<T>(index: number, directive: T, directiveDef: Di
export function directiveCreate<T>(
index: number, directive?: T, directiveDef?: DirectiveDef<T>): T {
let instance;
const index2 = index << 1;
if (directive == null) {
// return existing
ngDevMode && assertDirectivesInRange(index2);
instance = directives[index2];
ngDevMode && assertDataInRange(index);
instance = data[index];
} else {
ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex');
ngDevMode && assertPreviousIsParent();
@ -734,19 +742,26 @@ export function directiveCreate<T>(
}
previousOrParentNode !.flags = flags;
ngDevMode && assertDirectivesInRange(index2 - 1);
ngDevMode && assertDataInRange(index - 1);
Object.defineProperty(
directive, NG_HOST_SYMBOL, {enumerable: false, value: previousOrParentNode});
directives[index2] = instance = directive;
directives[index2 | 1] = directiveDef;
data[index] = instance = directive;
if (index >= ngStaticData.length) {
ngStaticData[index] = directiveDef !;
}
const diPublic = directiveDef !.diPublic;
if (diPublic) {
diPublic(directiveDef !);
}
const nodeBindings: LNodeStatic|null = previousOrParentNode.staticData;
if (nodeBindings && nodeBindings.attrs)
setInputsFromAttrs<T>(instance, directiveDef !.inputs, nodeBindings);
const staticData: LNodeStatic|null = previousOrParentNode.staticData !;
if (staticData && staticData.attrs) {
setInputsFromAttrs<T>(instance, directiveDef !.inputs, staticData);
}
}
return instance;
}
@ -936,31 +951,33 @@ export function viewCreate(viewBlockId: number): boolean {
enterView((existingView as LView).data, previousOrParentNode as LView);
} else {
// When we create a new View, we always reset the state of the instructions.
const newViewState = createViewState(viewBlockId, renderer);
const newViewState = createViewState(viewBlockId, renderer, initViewStaticData(viewBlockId, container));
enterView(newViewState, createLNode(null, LNodeFlags.View, null, newViewState));
containerState.nextIndex++;
}
setNgStaticDataForView(viewBlockId);
return !viewUpdateMode;
}
/**
* Each embedded view needs to set the global ngStaticData variable to the static data for that
* view.
* Otherwise, the view's static data for a particular node would overwrite the static
* data for a node in the view above it with the same index (since it's in the same template).
* Initialize the static data for the active view.
*
* Each embedded view needs to set the global ngStaticData variable to the static data for
* that view. Otherwise, the view's static data for a particular node would overwrite
* the staticdata for a node in the view above it with the same index (since it's in the
* same template).
*
* @param viewIndex The index of the view's static data in containerStatic
* @param parent The parent container in which to look for the view's static data
* @returns NgStaticData
*/
function setNgStaticDataForView(viewIndex: number): void {
ngDevMode && assertNodeType(previousOrParentNode.parent !, LNodeFlags.Container);
const containerStatic =
(previousOrParentNode.parent !.staticData as LContainerStatic).containerStatic;
function initViewStaticData(viewIndex: number, parent: LContainer): NgStaticData {
ngDevMode && assertNodeType(parent, LNodeFlags.Container);
const containerStatic = (parent !.staticData as LContainerStatic).containerStatic;
if (viewIndex >= containerStatic.length || containerStatic[viewIndex] == null) {
containerStatic[viewIndex] = [];
}
ngStaticData = containerStatic[viewIndex];
return containerStatic[viewIndex];
}
/**
@ -998,18 +1015,14 @@ export const refreshComponent:
const element = data ![elementIndex] as LElement;
ngDevMode && assertNodeType(element, LNodeFlags.Element);
ngDevMode && assertNotEqual(element.data, null, 'isComponent');
ngDevMode && assertDirectivesInRange(directiveIndex << 1);
ngDevMode && assertDataInRange(directiveIndex);
const hostView = element.data !;
ngDevMode && assertNotEqual(hostView, null, 'hostView');
const directive = directives[directiveIndex << 1];
const directive = data[directiveIndex];
const oldView = enterView(hostView, element);
const oldNgStaticData = ngStaticData;
try {
const _template = template || this !.template;
ngStaticData = _template.ngStaticData || (_template.ngStaticData = [] as never);
_template(directive, creationMode);
(template || this !.template)(directive, creationMode);
} finally {
ngStaticData = oldNgStaticData;
leaveView(oldView);
}
};
@ -1557,6 +1570,11 @@ function valueInData<T>(data: any[], index: number, value?: T): T {
if (value === undefined) {
value = data[index];
} else {
// We don't store any static data for local variables, so the first time
// we see the template, we should store as null to avoid a sparse array
if (index >= ngStaticData.length) {
ngStaticData[index] = null;
}
data[index] = value;
}
return value !;
@ -1585,6 +1603,3 @@ function assertDataInRange(index: number, arr?: any[]) {
assertLessThan(arr ? arr.length : 0, index, 'data.length');
}
function assertDirectivesInRange(index: number) {
assertLessThan(directives ? directives.length : 0, index, 'directives.length');
}

View File

@ -7,7 +7,7 @@
*/
import {ElementRef, Injector, QueryList, TemplateRef, Type, ViewContainerRef} from '../core';
import {ComponentTemplate} from './public_interfaces';
import {ComponentTemplate, DirectiveDef} from './public_interfaces';
import {RComment, RElement, RText, Renderer3} from './renderer';
declare global {
@ -49,8 +49,7 @@ export const enum LNodeFlags {
* being processed and restored when the child `View` is done.
*
* Keeping separate state for each view facilities view insertion / deletion, so we
* don't have to edit the nodes array or directives array based on which views
* are present.
* don't have to edit the data array based on which views are present.
*/
export interface ViewState {
/**
@ -79,38 +78,6 @@ export interface ViewState {
*/
readonly renderer: Renderer3;
/**
* This array stores all element/text/container nodes created inside this view
* and their bindings. Stored as an array rather than a linked list so we can
* look up nodes directly in the case of forward declaration or bindings
* (e.g. E(1))..
*
* All bindings for a given view are stored in the order in which they
* appear in the template, starting with `bindingStartIndex`.
* We use `bindingIndex` to internally keep track of which binding
* is currently active.
*
* NOTE: We also use nodes == null as a marker for creationMode. We
* do this by creating ViewState in incomplete state with nodes == null
* and we initialize it on first run.
*/
readonly data: any[];
/**
* All directives created inside this view. Stored as an array
* rather than a linked list so we can look up directives directly
* in the case of forward declaration or DI.
*
* The array alternates between instances and directive tokens.
* - even indices: contain the directive token (type)
* - odd indices: contain the directive def
*
* We must store the directive def (rather than token | null)
* because we need to be able to access the inputs and outputs
* of directives that aren't diPublic.
*/
readonly directives: any[];
/**
* The binding start index is the index at which the nodes array
* starts to store bindings only. Saving this value ensures that we
@ -159,6 +126,30 @@ export interface ViewState {
* in the same container. We need a way to link component views as well.
*/
next: ViewState|ContainerState|null;
/**
* This array stores all element/text/container nodes created inside this view
* and their bindings. Stored as an array rather than a linked list so we can
* look up nodes directly in the case of forward declaration or bindings
* (e.g. E(1))..
*
* All bindings for a given view are stored in the order in which they
* appear in the template, starting with `bindingStartIndex`.
* We use `bindingIndex` to internally keep track of which binding
* is currently active.
*
* NOTE: We also use data == null as a marker for creationMode. We
* do this by creating ViewState in incomplete state with nodes == null
* and we initialize it on first run.
*/
readonly data: any[];
/**
* The static data array for the current view. We need a reference to this so we
* can easily walk up the node tree in DI and get the ngStaticData array associated
* with a node (where the directive defs are stored).
*/
ngStaticData: (LNodeStatic|DirectiveDef<any>|null)[];
}
export interface LNodeInjector {
@ -169,9 +160,9 @@ export interface LNodeInjector {
readonly parent: LNodeInjector|null;
/**
* Allows access to the directives array in that node's view and to
* Allows access to the directives array in that node's static data and to
* the node's flags (for starting directive index and directive size). Necessary
* for DI to retrieve a directive from the directives array if injector indicates
* for DI to retrieve a directive from the data array if injector indicates
* it is there.
*/
readonly node: LElement|LContainer;
@ -446,6 +437,9 @@ export type InitialInputData = (InitialInputs | null)[];
*/
export type InitialInputs = string[];
/** The type of the global ngStaticData array. */
export type NgStaticData = (LNodeStatic | DirectiveDef<any> | null)[];
/**
* LNode binding data for a particular node that is shared between all templates
* of a specific type.

View File

@ -10,6 +10,7 @@ import {Observable} from 'rxjs/Observable';
import {QueryList as IQueryList, Type} from '../core';
import {assertNotNull} from './assert';
import {LContainer, LNode, LNodeFlags, LView, QueryState} from './interfaces';
import {DirectiveDef} from '@angular/core/src/render3/public_interfaces';
@ -101,14 +102,14 @@ function add(predicate: QueryPredicate<any>| null, node: LNode) {
while (predicate) {
const type = predicate.type;
if (type) {
const directives = node.view.directives;
const ngStaticData = node.view.ngStaticData;
const flags = node.flags;
for (let i = flags >> LNodeFlags.INDX_SHIFT,
ii = i + ((flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT);
i < ii; i++) {
const def = directives[i << 1 | 1];
const def = ngStaticData[i] as DirectiveDef<any>;
if (def.diPublic && def.type === type) {
predicate.values.push(directives[i << 1]);
predicate.values.push(node.view.data[i]);
}
}
}