diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index c82de9358f80..a5b98c2c7c21 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -19,13 +19,14 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, ViewRef as viewEngine_Vie import {Type} from '../type'; import {assertGreaterThan, assertLessThan, assertNotNull} from './assert'; -import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; +import {addToViewTree, assertPreviousIsParent, createLContainer, createLNodeObject, createTView, getDirectiveInstance, getPreviousOrParentNode, getRenderer, isComponent, renderEmbeddedTemplate, resolveDirective} from './instructions'; +import {LContainer} from './interfaces/container'; import {ComponentTemplate, DirectiveDef, DirectiveDefList, PipeDefList} from './interfaces/definition'; import {LInjector} from './interfaces/injector'; import {LContainerNode, LElementNode, LNode, LNodeType, LViewNode, TNodeFlags} from './interfaces/node'; import {QueryReadType} from './interfaces/query'; import {Renderer3} from './interfaces/renderer'; -import {LView} from './interfaces/view'; +import {LView, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; import {insertView, removeView} from './node_manipulation'; import {notImplemented, stringify} from './util'; @@ -568,7 +569,6 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer const vcRefHost = di.node; ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element); - const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view); const lContainerNode: LContainerNode = createLNodeObject( LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null); @@ -695,29 +695,35 @@ class ViewContainerRef implements viewEngine_ViewContainerRef { * @returns The TemplateRef instance to use */ export function getOrCreateTemplateRef(di: LInjector): viewEngine_TemplateRef { - ngDevMode && assertNodeType(di.node, LNodeType.Container); - const data = (di.node as LContainerNode).data; - const tView = di.node.view.tView; - return di.templateRef || (di.templateRef = new TemplateRef( - getOrCreateElementRef(di), data.template !, getRenderer(), - tView.directiveRegistry, tView.pipeRegistry)); + if (!di.templateRef) { + ngDevMode && assertNodeType(di.node, LNodeType.Container); + const hostNode = di.node as LContainerNode; + const hostTNode = hostNode.tNode !; + const hostTView = hostNode.view.tView; + if (!hostTNode.tViews) { + hostTNode.tViews = createTView(hostTView.directiveRegistry, hostTView.pipeRegistry); + } + ngDevMode && assertNotNull(hostTNode.tViews, 'TView must be allocated'); + di.templateRef = new TemplateRef( + getOrCreateElementRef(di), hostTNode.tViews as TView, hostNode.data.template !, + getRenderer(), hostTView.directiveRegistry, hostTView.pipeRegistry); + } + return di.templateRef; } class TemplateRef implements viewEngine_TemplateRef { readonly elementRef: viewEngine_ElementRef; - private _template: ComponentTemplate; constructor( - elementRef: viewEngine_ElementRef, template: ComponentTemplate, - private _renderer: Renderer3, private _directives: DirectiveDefList|null, - private _pipes: PipeDefList|null) { + elementRef: viewEngine_ElementRef, private _tView: TView, + private _template: ComponentTemplate, private _renderer: Renderer3, + private _directives: DirectiveDefList|null, private _pipes: PipeDefList|null) { this.elementRef = elementRef; - this._template = template; } createEmbeddedView(context: T): viewEngine_EmbeddedViewRef { const viewNode = renderEmbeddedTemplate( - null, this._template, context, this._renderer, this._directives, this._pipes); + null, this._tView, this._template, context, this._renderer, this._directives, this._pipes); return addDestroyable(new EmbeddedViewRef(viewNode, this._template, context)); } } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index dcddb6588cf2..86b4ea07ea8f 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -9,7 +9,7 @@ import './ng_dev_mode'; import {assertEqual, assertLessThan, assertNotEqual, assertNotNull, assertNull, assertSame} from './assert'; -import {LContainer, TContainer} from './interfaces/container'; +import {LContainer} from './interfaces/container'; import {LInjector} from './interfaces/injector'; import {CssSelectorList, LProjection, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'; import {LQueries} from './interfaces/query'; @@ -468,9 +468,20 @@ export function renderTemplate( return host; } +/** + * Used for rendering embedded views (e.g. dynamically created views) + * + * Dynamically created views must store/retrieve their TViews differently from component views + * because their template functions are nested in the template functions of their hosts, creating + * closures. If their host template happens to be an embedded template in a loop (e.g. ngFor inside + * an ngFor), the nesting would mean we'd have multiple instances of the template function, so we + * can't store TViews in the template function itself (as we do for comps). Instead, we store the + * TView for dynamically created views on their host TNode, which only has one instance. + */ export function renderEmbeddedTemplate( - viewNode: LViewNode | null, template: ComponentTemplate, context: T, renderer: Renderer3, - directives?: DirectiveDefList | null, pipes?: PipeDefList | null): LViewNode { + viewNode: LViewNode | null, tView: TView, template: ComponentTemplate, context: T, + renderer: Renderer3, directives?: DirectiveDefList | null, + pipes?: PipeDefList | null): LViewNode { const _isParent = isParent; const _previousOrParentNode = previousOrParentNode; let oldView: LView; @@ -480,7 +491,6 @@ export function renderEmbeddedTemplate( previousOrParentNode = null !; if (viewNode == null) { - const tView = getOrCreateTView(template, directives || null, pipes || null); const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways); viewNode = createLNode(null, LNodeType.View, null, lView); @@ -566,22 +576,33 @@ export function elementStart( assertEqual( currentView.bindingStartIndex, -1, 'elements should be created before any bindings'); + ngDevMode && ngDevMode.rendererCreateElement++; const native: RElement = renderer.createElement(name); const node: LElementNode = createLNode(index, LNodeType.Element, native !, null); if (attrs) setUpAttributes(native, attrs); appendChild(node.parent !, native, currentView); - createDirectivesAndLocals(index, name, attrs, localRefs, null); + createDirectivesAndLocals(index, name, attrs, localRefs, false); return native; } +/** + * Creates directive instances and populates local refs. + * + * @param index Index of the current node (to create TNode) + * @param name Tag name of the current node + * @param attrs Attrs of the current node + * @param localRefs Local refs of the current node + * @param inlineViews Whether or not this node will create inline views + */ function createDirectivesAndLocals( index: number, name: string | null, attrs: string[] | null | undefined, - localRefs: string[] | null | undefined, containerData: TView[] | null) { + localRefs: string[] | null | undefined, inlineViews: boolean) { const node = previousOrParentNode; if (firstTemplatePass) { + ngDevMode && ngDevMode.firstTemplatePass++; ngDevMode && assertDataInRange(index - 1); - node.tNode = tData[index] = createTNode(name, attrs || null, containerData); + node.tNode = tData[index] = createTNode(name, attrs || null, inlineViews ? [] : null); cacheMatchingDirectivesForNode(node.tNode, currentView.tView, localRefs || null); } else { instantiateDirectivesDirectly(); @@ -749,6 +770,13 @@ function saveResolvedLocalsInData(): void { function getOrCreateTView( template: ComponentTemplate, directives: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView { + // TODO(misko): reading `ngPrivateData` here is problematic for two reasons + // 1. It is a megamorphic call on each invocation. + // 2. For nested embedded views (ngFor inside ngFor) the template instance is per + // outer template invocation, which means that no such property will exist + // Correct solution is to only put `ngPrivateData` on the Component template + // and not on embedded templates. + return template.ngPrivateData || (template.ngPrivateData = createTView(directives, pipes) as never); } @@ -756,6 +784,7 @@ function getOrCreateTView( /** Creates a TView instance */ export function createTView( defs: DirectiveDefListOrFactory | null, pipes: PipeDefListOrFactory | null): TView { + ngDevMode && ngDevMode.tView++; return { data: [], directives: null, @@ -784,6 +813,7 @@ function setUpAttributes(native: RElement, attrs: string[]): void { const attrName = attrs[i]; if (attrName !== NG_PROJECT_AS_ATTR_NAME) { const attrVal = attrs[i + 1]; + ngDevMode && ngDevMode.rendererSetAttribute++; isProc ? (renderer as ProceduralRenderer3).setAttribute(native, attrName, attrVal) : native.setAttribute(attrName, attrVal); } @@ -867,6 +897,7 @@ export function listener( // In order to match current behavior, native DOM event listeners must be added for all // events (including outputs). const cleanupFns = cleanup || (cleanup = currentView.cleanup = []); + ngDevMode && ngDevMode.rendererAddEventListener++; if (isProceduralRenderer(renderer)) { const wrappedListener = wrapListenerWithDirtyLogic(currentView, listenerFn); const cleanupFn = renderer.listen(native, eventName, wrappedListener); @@ -931,9 +962,11 @@ export function elementAttribute( if (value !== NO_CHANGE) { const element: LElementNode = data[index]; if (value == null) { + ngDevMode && ngDevMode.rendererRemoveAttribute++; isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) : element.native.removeAttribute(name); } else { + ngDevMode && ngDevMode.rendererSetAttribute++; const strValue = sanitizer == null ? stringify(value) : sanitizer(value); isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) : element.native.setAttribute(name, strValue); @@ -977,6 +1010,7 @@ export function elementProperty( // is risky, so sanitization can be done without further checks. value = sanitizer != null ? (sanitizer(value) as any) : value; const native = node.native; + ngDevMode && ngDevMode.rendererSetProperty++; isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) : (native.setProperty ? native.setProperty(propName, value) : (native as any)[propName] = value); @@ -986,14 +1020,15 @@ export function elementProperty( /** * Constructs a TNode object from the arguments. * - * @param tagName - * @param attrs - * @param data + * @param tagName The tag name of the node + * @param attrs The attributes defined on this ndoe + * @param tViews Any TViews attached to this node * @param localNames A list of local names and their matching indices * @returns the TNode object */ function createTNode( - tagName: string | null, attrs: string[] | null, data: TContainer | null): TNode { + tagName: string | null, attrs: string[] | null, tViews: TView[] | null): TNode { + ngDevMode && ngDevMode.tNode++; return { flags: 0, tagName: tagName, @@ -1002,7 +1037,7 @@ function createTNode( initialInputs: undefined, inputs: undefined, outputs: undefined, - data: data + tViews: tViews }; } @@ -1067,10 +1102,12 @@ export function elementClassNamed(index: number, className: string, value: T if (value !== NO_CHANGE) { const lElement = data[index] as LElementNode; if (value) { + ngDevMode && ngDevMode.rendererAddClass++; isProceduralRenderer(renderer) ? renderer.addClass(lElement.native, className) : lElement.native.classList.add(className); } else { + ngDevMode && ngDevMode.rendererRemoveClass++; isProceduralRenderer(renderer) ? renderer.removeClass(lElement.native, className) : lElement.native.classList.remove(className); } @@ -1095,6 +1132,7 @@ export function elementClass(index: number, value: T | NO_CHANGE): void { // future // we will add logic here which would work with the animation code. const lElement: LElementNode = data[index]; + ngDevMode && ngDevMode.rendererSetClassName++; isProceduralRenderer(renderer) ? renderer.setProperty(lElement.native, 'className', value) : lElement.native['className'] = stringify(value); } @@ -1121,6 +1159,7 @@ export function elementStyleNamed( if (value !== NO_CHANGE) { const lElement: LElementNode = data[index]; if (value == null) { + ngDevMode && ngDevMode.rendererRemoveStyle++; isProceduralRenderer(renderer) ? renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) : lElement.native['style'].removeProperty(styleName); @@ -1128,6 +1167,7 @@ export function elementStyleNamed( let strValue = typeof suffixOrSanitizer == 'function' ? suffixOrSanitizer(value) : stringify(value); if (typeof suffixOrSanitizer == 'string') strValue = strValue + suffixOrSanitizer; + ngDevMode && ngDevMode.rendererSetStyle++; isProceduralRenderer(renderer) ? renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) : lElement.native['style'].setProperty(styleName, strValue); @@ -1155,14 +1195,20 @@ export function elementStyle( // we will add logic here which would work with the animation code. const lElement = data[index] as LElementNode; if (isProceduralRenderer(renderer)) { + ngDevMode && ngDevMode.rendererSetStyle++; renderer.setProperty(lElement.native, 'style', value); } else { const style = lElement.native['style']; for (let i = 0, keys = Object.keys(value); i < keys.length; i++) { const styleName: string = keys[i]; const styleValue: any = (value as any)[styleName]; - styleValue == null ? style.removeProperty(styleName) : - style.setProperty(styleName, styleValue); + if (styleValue == null) { + ngDevMode && ngDevMode.rendererRemoveStyle++; + style.removeProperty(styleName); + } else { + ngDevMode && ngDevMode.rendererSetStyle++; + style.setProperty(styleName, styleValue); + } } } } @@ -1184,6 +1230,7 @@ export function text(index: number, value?: any): void { ngDevMode && assertEqual( currentView.bindingStartIndex, -1, 'text nodes should be created before bindings'); + ngDevMode && ngDevMode.rendererCreateTextNode++; const textNode = createTextNode(value, renderer); const node = createLNode(index, LNodeType.Element, textNode); // Text nodes are self closing. @@ -1203,6 +1250,7 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { let existingNode = data[index] as LTextNode; ngDevMode && assertNotNull(existingNode, 'LNode should exist'); ngDevMode && assertNotNull(existingNode.native, 'native element should exist'); + ngDevMode && ngDevMode.rendererSetText++; value !== NO_CHANGE && (isProceduralRenderer(renderer) ? renderer.setValue(existingNode.native, stringify(value)) : existingNode.native.textContent = stringify(value)); @@ -1407,7 +1455,7 @@ export function createLContainer( * @param localRefs A set of local reference bindings on the element. */ export function container( - index: number, template?: ComponentTemplate, tagName?: string, attrs?: string[], + index: number, template?: ComponentTemplate, tagName?: string | null, attrs?: string[], localRefs?: string[] | null): void { ngDevMode && assertEqual( currentView.bindingStartIndex, -1, @@ -1421,7 +1469,7 @@ export function container( // Containers are added to the current view tree instead of their embedded views // because views can be removed and re-inserted. addToViewTree(currentView, node.data); - createDirectivesAndLocals(index, tagName || null, attrs, localRefs, []); + createDirectivesAndLocals(index, tagName || null, attrs, localRefs, template == null); isParent = false; ngDevMode && assertNodeType(previousOrParentNode, LNodeType.Container); @@ -1486,9 +1534,12 @@ function refreshDynamicChildren() { if (current.dynamicViewCount !== 0 && (current as LContainer).views) { const container = current as LContainer; for (let i = 0; i < container.views.length; i++) { - const view = container.views[i]; + const lViewNode = container.views[i]; // The directives and pipes are not needed here as an existing view is only being refreshed. - renderEmbeddedTemplate(view, view.data.template !, view.data.context !, renderer); + const dynamicView = lViewNode.data; + ngDevMode && assertNotNull(dynamicView.tView, 'TView must be allocated'); + renderEmbeddedTemplate( + lViewNode, dynamicView.tView, dynamicView.template !, dynamicView.context !, renderer); } } } @@ -1558,23 +1609,24 @@ export function embeddedViewStart(viewBlockId: number): RenderFlags { /** * Initialize the TView (e.g. static data) for the active embedded view. * - * Each embedded view needs to set the global tData 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). + * Each embedded view block must create or retrieve its own TView. Otherwise, the embedded 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). * - * @param viewIndex The index of the TView in TContainer + * @param viewIndex The index of the TView in TNode.tViews * @param parent The parent container in which to look for the view's static data * @returns TView */ function getOrCreateEmbeddedTView(viewIndex: number, parent: LContainerNode): TView { ngDevMode && assertNodeType(parent, LNodeType.Container); - const tContainer = (parent !.tNode as TContainerNode).data; - if (viewIndex >= tContainer.length || tContainer[viewIndex] == null) { + const containerTViews = (parent !.tNode as TContainerNode).tViews as TView[]; + ngDevMode && assertNotNull(containerTViews, 'TView expected'); + ngDevMode && assertEqual(Array.isArray(containerTViews), true, 'TViews should be in an array'); + if (viewIndex >= containerTViews.length || containerTViews[viewIndex] == null) { const tView = currentView.tView; - tContainer[viewIndex] = createTView(tView.directiveRegistry, tView.pipeRegistry); + containerTViews[viewIndex] = createTView(tView.directiveRegistry, tView.pipeRegistry); } - return tContainer[viewIndex]; + return containerTViews[viewIndex]; } /** Marks the end of an embedded view. */ diff --git a/packages/core/src/render3/interfaces/container.ts b/packages/core/src/render3/interfaces/container.ts index 46e0a54966da..be4fedb11b40 100644 --- a/packages/core/src/render3/interfaces/container.ts +++ b/packages/core/src/render3/interfaces/container.ts @@ -82,24 +82,6 @@ export interface LContainer { queries: LQueries|null; } -/** - * The static equivalent of LContainer, used in TContainerNode. - * - * The container needs to store static data for each of its embedded views - * (TViews). Otherwise, nodes in embedded views with the same index as nodes - * in their parent views will overwrite each other, as they are in - * the same template. - * - * Each index in this array corresponds to the static data for a certain - * view. So if you had V(0) and V(1) in a container, you might have: - * - * [ - * [{tagName: 'div', attrs: ...}, null], // V(0) TView - * [{tagName: 'button', attrs ...}, null] // V(1) TView - * ] - */ -export type TContainer = TView[]; - // Note: This hack is necessary so we don't erroneously get a circular dependency // failure based on types. export const unusedValueExportToPlacateAjd = 1; diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 691e49209513..0ec9adeb2fcc 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {LContainer, TContainer} from './container'; +import {LContainer} from './container'; import {LInjector} from './injector'; import {LProjection} from './projection'; import {LQueries} from './query'; @@ -283,21 +283,33 @@ export interface TNode { outputs: PropertyAliases|null|undefined; /** - * The static data equivalent of LNode.data. + * The TView or TViews attached to this node. * - * If this TNode corresponds to an LContainerNode, the container will - * need to store separate static data for each of its views (TContainer). + * If this TNode corresponds to an LContainerNode with inline views, the container will + * need to store separate static data for each of its view blocks (TView[]). Otherwise, + * nodes in inline views with the same index as nodes in their parent views will overwrite + * each other, as they are in the same template. * - * If this TNode corresponds to an LElementNode, data will be null. + * Each index in this array corresponds to the static data for a certain + * view. So if you had V(0) and V(1) in a container, you might have: + * + * [ + * [{tagName: 'div', attrs: ...}, null], // V(0) TView + * [{tagName: 'button', attrs ...}, null] // V(1) TView + * + * If this TNode corresponds to an LContainerNode with a template (e.g. structural + * directive), the template's TView will be stored here. + * + * If this TNode corresponds to an LElementNode, tViews will be null . */ - data: TContainer|null; + tViews: TView|TView[]|null; } /** Static data for an LElementNode */ -export interface TElementNode extends TNode { data: null; } +export interface TElementNode extends TNode { tViews: null; } /** Static data for an LContainerNode */ -export interface TContainerNode extends TNode { data: TContainer; } +export interface TContainerNode extends TNode { tViews: TView|TView[]|null; } /** * This mapping is necessary so we can set input properties and output listeners diff --git a/packages/core/src/render3/ng_dev_mode.ts b/packages/core/src/render3/ng_dev_mode.ts index 00fcad5c5dd3..358060538636 100644 --- a/packages/core/src/render3/ng_dev_mode.ts +++ b/packages/core/src/render3/ng_dev_mode.ts @@ -8,15 +8,51 @@ declare global { - const ngDevMode: boolean; + const ngDevMode: null|NgDevModePerfCounters; + interface NgDevModePerfCounters { + firstTemplatePass: number; + tNode: number; + tView: number; + rendererCreateTextNode: number; + rendererSetText: number; + rendererCreateElement: number; + rendererAddEventListener: number; + rendererSetAttribute: number; + rendererRemoveAttribute: number; + rendererSetProperty: number; + rendererSetClassName: number; + rendererAddClass: number; + rendererRemoveClass: number; + rendererSetStyle: number; + rendererRemoveStyle: number; + } } -declare let global: any; -if (typeof ngDevMode == 'undefined') { - if (typeof window != 'undefined') (window as any).ngDevMode = true; - if (typeof self != 'undefined') (self as any).ngDevMode = true; - if (typeof global != 'undefined') (global as any).ngDevMode = true; -} -export const _ngDevMode = true; +declare let global: any; +export const ngDevModeResetPerfCounters: () => void = + (typeof ngDevMode == 'undefined' && (function(global: {ngDevMode: NgDevModePerfCounters}) { + function ngDevModeResetPerfCounters() { + global['ngDevMode'] = { + firstTemplatePass: 0, + tNode: 0, + tView: 0, + rendererCreateTextNode: 0, + rendererSetText: 0, + rendererCreateElement: 0, + rendererAddEventListener: 0, + rendererSetAttribute: 0, + rendererRemoveAttribute: 0, + rendererSetProperty: 0, + rendererSetClassName: 0, + rendererAddClass: 0, + rendererRemoveClass: 0, + rendererSetStyle: 0, + rendererRemoveStyle: 0, + }; + } + ngDevModeResetPerfCounters(); + return ngDevModeResetPerfCounters; + })(typeof window != 'undefined' && window || typeof self != 'undefined' && self || + typeof global != 'undefined' && global)) as() => void; diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index c654954a7fb3..224b17c436ab 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -6,12 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, renderTemplate} from '../../src/render3/instructions'; +import {NgForOfContext} from '@angular/common'; + +import {RenderFlags, directiveInject} from '../../src/render3'; +import {defineComponent} from '../../src/render3/definition'; +import {bind, container, elementAttribute, elementClass, elementEnd, elementProperty, elementStart, elementStyle, elementStyleNamed, interpolation1, renderTemplate, text, textBinding} from '../../src/render3/instructions'; import {LElementNode, LNode} from '../../src/render3/interfaces/node'; import {RElement, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, sanitizeStyle, sanitizeUrl} from '../../src/sanitization/sanitization'; -import {TemplateFixture} from './render_util'; +import {NgForOf} from './common_with_def'; +import {ComponentFixture, TemplateFixture} from './render_util'; describe('instructions', () => { function createDiv() { @@ -30,6 +35,13 @@ describe('instructions', () => { () => elementAttribute( 0, 'title', bypassSanitizationTrustUrl('javascript:true'), sanitizeUrl)); expect(t.html).toEqual('
'); + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 1, + tNode: 1, + tView: 1, + rendererCreateElement: 1, + rendererSetAttribute: 2 + }); }); }); @@ -44,6 +56,12 @@ describe('instructions', () => { () => elementProperty( 0, 'title', bypassSanitizationTrustUrl('javascript:false'), sanitizeUrl)); expect(t.html).toEqual('
'); + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 1, + tNode: 1, + tView: 1, + rendererCreateElement: 1, + }); }); it('should not stringify non string values', () => { @@ -52,6 +70,13 @@ describe('instructions', () => { t.update(() => elementProperty(0, 'hidden', false)); // The hidden property would be true if `false` was stringified into `"false"`. expect((t.hostNode.native as HTMLElement).querySelector('div') !.hidden).toEqual(false); + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 1, + tNode: 1, + tView: 1, + rendererCreateElement: 1, + rendererSetProperty: 1 + }); }); }); @@ -77,20 +102,79 @@ describe('instructions', () => { elementStart(0, 'div', ['style', 'height: 10px']); elementEnd(); } - const fixture = new TemplateFixture(createDivWithStyle); it('should add style', () => { + const fixture = new TemplateFixture(createDivWithStyle); fixture.update(() => elementStyle(0, {'background-color': 'red'})); expect(fixture.html).toEqual('
'); }); }); describe('elementClass', () => { - const fixture = new TemplateFixture(createDiv); it('should add class', () => { + const fixture = new TemplateFixture(createDiv); fixture.update(() => elementClass(0, 'multiple classes')); expect(fixture.html).toEqual('
'); }); }); + + describe('performance counters', () => { + it('should create tViews only once for each nested level', () => { + const _c0 = ['ngFor', '', 'ngForOf', '']; + /** + *
    + *
  • {{col}}
  • + *
+ */ + class NestedLoops { + rows = [['a', 'b'], ['A', 'B'], ['a', 'b'], ['A', 'B']]; + + static ngComponentDef = defineComponent({ + type: NestedLoops, + selectors: [['nested-loops']], + factory: function ToDoAppComponent_Factory() { return new NestedLoops(); }, + template: function ToDoAppComponent_Template(rf: RenderFlags, ctx: NestedLoops) { + if (rf & RenderFlags.Create) { + container(0, ToDoAppComponent_NgForOf_Template_0, null, _c0); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngForOf', bind(ctx.rows)); + } + function ToDoAppComponent_NgForOf_Template_0( + rf: RenderFlags, ctx0: NgForOfContext) { + if (rf & RenderFlags.Create) { + elementStart(0, 'ul'); + container(1, ToDoAppComponent_NgForOf_NgForOf_Template_1, null, _c0); + elementEnd(); + } + if (rf & RenderFlags.Update) { + const row_r2 = ctx0.$implicit; + elementProperty(1, 'ngForOf', bind(row_r2)); + } + function ToDoAppComponent_NgForOf_NgForOf_Template_1( + rf: RenderFlags, ctx1: NgForOfContext) { + if (rf & RenderFlags.Create) { + elementStart(0, 'li'); + text(1); + elementEnd(); + } + if (rf & RenderFlags.Update) { + const col_r3 = ctx1.$implicit; + textBinding(1, interpolation1('', col_r3, '')); + } + } + } + }, + directives: [NgForOf] + }); + } + const fixture = new ComponentFixture(NestedLoops); + expect(ngDevMode).toHaveProperties({ + // Expect: fixture view/Host view + component + ngForRow + ngForCol + tView: 4, // should be: 4, + }); + + }); + }); }); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index deb02afc3ee6..d1ae1f67a58f 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -28,6 +28,12 @@ describe('render3 integration test', () => { elementEnd(); } } + expect(ngDevMode).toHaveProperties({ + firstTemplatePass: 1, + tNode: 1, + tView: 1, + rendererCreateElement: 1, + }); }); it('should render and update basic "Hello, World" template', () => { @@ -824,7 +830,7 @@ describe('render3 integration test', () => { const oldTemplateData = (Template as any).ngPrivateData; const oldContainerData = (oldTemplateData as any).data[0]; - const oldElementData = oldContainerData.data[0][0]; + const oldElementData = oldContainerData.tViews[0][0]; expect(oldContainerData).not.toBeNull(); expect(oldElementData).not.toBeNull(); @@ -833,7 +839,7 @@ describe('render3 integration test', () => { const newTemplateData = (Template as any).ngPrivateData; const newContainerData = (oldTemplateData as any).data[0]; - const newElementData = oldContainerData.data[0][0]; + const newElementData = oldContainerData.tViews[0][0]; expect(newTemplateData === oldTemplateData).toBe(true); expect(newContainerData === oldContainerData).toBe(true); expect(newElementData === oldElementData).toBe(true); diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts index d04a9adaecde..e47cc4aab8f7 100644 --- a/packages/core/test/render3/node_selector_matcher_spec.ts +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -19,7 +19,7 @@ function testLStaticData(tagName: string, attrs: string[] | null): TNode { initialInputs: undefined, inputs: undefined, outputs: undefined, - data: null, + tViews: null, }; } diff --git a/packages/core/test/render3/perfCounter_spec.ts b/packages/core/test/render3/perfCounter_spec.ts new file mode 100644 index 000000000000..b98b54ff3b6e --- /dev/null +++ b/packages/core/test/render3/perfCounter_spec.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 {ngDevModeResetPerfCounters} from '../../src/render3/ng_dev_mode'; + +beforeEach(ngDevModeResetPerfCounters); +beforeEach(() => { + jasmine.addMatchers({ + toHaveProperties: function(util, customEqualityTesters) { + return {compare: toHavePropertiesCompare}; + } + }); +}); +function toHavePropertiesCompare(actual: any, expected: any) { + let pass = true; + let errors = []; + for (let key of Object.keys(actual)) { + if (expected.hasOwnProperty(key)) { + if (actual[key] !== expected[key]) { + pass = false; + errors.push(`Expected '${key}' to be '${expected[key]}' but was '${actual[key]}'.`); + } + } + } + return {pass: pass, message: errors.join('\n')}; +} + +describe('toHaveProperties', () => { + it('should pass', () => { + expect({tNode: 1}).toHaveProperties({}); + expect({tNode: 2}).toHaveProperties({tNode: 2}); + }); + + it('should fail', () => { + expect(toHavePropertiesCompare({tNode: 2, tView: 4}, {tNode: 3, tView: 5})).toEqual({ + pass: false, + message: + 'Expected \'tNode\' to be \'3\' but was \'2\'.\nExpected \'tView\' to be \'5\' but was \'4\'.' + }); + }); +}); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 98b3781da764..91563976de4e 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -147,30 +147,24 @@ describe('ViewContainerRef', () => { const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); elementProperty(0, 'tplRef', bind(tplRef)); containerRefreshStart(0); - let rf1 = embeddedViewStart(1); - if (rf1 & RenderFlags.Create) { - elementStart(0, 'header'); - elementEnd(); - } - embeddedViewEnd(); containerRefreshEnd(); } const fixture = new TemplateFixture(createTemplate, updateTemplate, [DirectiveWithVCRef]); - expect(fixture.html).toEqual('
'); + expect(fixture.html).toEqual('
'); createView('A'); fixture.update(); - expect(fixture.html).toEqual('
A
'); + expect(fixture.html).toEqual('A
'); createView('B'); createView('C'); fixture.update(); - expect(fixture.html).toEqual('
ABC
'); + expect(fixture.html).toEqual('ABC
'); createView('Y', 0); fixture.update(); - expect(fixture.html).toEqual('
YABC
'); + expect(fixture.html).toEqual('YABC
'); expect(() => { createView('Z', -1); }).toThrow(); expect(() => { createView('Z', 5); }).toThrow(); diff --git a/packages/types.d.ts b/packages/types.d.ts index 1ec1d9f1e989..ad0b87d80255 100644 --- a/packages/types.d.ts +++ b/packages/types.d.ts @@ -18,4 +18,10 @@ /// declare let isNode: boolean; -declare let isBrowser: boolean; \ No newline at end of file +declare let isBrowser: boolean; + +declare namespace jasmine { + interface Matchers { + toHaveProperties(obj: any): boolean; + } +}