perf(compiler): introduce direct rendering

This allows to attach / detach embedded views and projected nodes
in a faster way.
This commit is contained in:
Tobias Bosch 2016-11-02 17:54:05 -07:00 committed by vikerman
parent d708a8859c
commit 9c23884da4
9 changed files with 211 additions and 20 deletions

View File

@ -156,13 +156,7 @@ class ViewBuilderVisitor implements TemplateAstVisitor {
var parentRenderNode = this._getParentRenderNode(parent);
if (parentRenderNode !== o.NULL_EXPR) {
this.view.createMethod.addStmt(
ViewProperties.renderer
.callMethod(
'projectNodes',
[
parentRenderNode,
o.THIS_EXPR.callMethod('projectedNodes', [o.literal(ast.index)])
])
o.THIS_EXPR.callMethod('projectNodes', [parentRenderNode, o.literal(ast.index)])
.toStmt());
} else if (this._isRootNode(parent)) {
if (this.view.viewType !== ViewType.COMPONENT) {
@ -533,13 +527,17 @@ function generateCreateMethod(view: CompileView): o.Statement[] {
} else {
resultExpr = o.NULL_EXPR;
}
let allNodesExpr: o.Expression =
ViewProperties.renderer.prop('directRenderer')
.conditional(o.NULL_EXPR, o.literalArr(view.nodes.map(node => node.renderNode)));
return parentRenderNodeStmts.concat(view.createMethod.finish(), [
o.THIS_EXPR
.callMethod(
'init',
[
view.lastRenderNode,
o.literalArr(view.nodes.map(node => node.renderNode)),
allNodesExpr,
view.disposables.length ? o.literalArr(view.disposables) : o.NULL_EXPR,
])
.toStmt(),

View File

@ -75,7 +75,9 @@ export var __core_private__: {
UNINITIALIZED: typeof change_detection_util.UNINITIALIZED,
ValueUnwrapper: typeof change_detection_util.ValueUnwrapper,
_ValueUnwrapper?: change_detection_util.ValueUnwrapper,
RenderDebugInfo: typeof api.RenderDebugInfo, _RenderDebugInfo?: api.RenderDebugInfo,
RenderDebugInfo: typeof api.RenderDebugInfo,
_RenderDebugInfo?: api.RenderDebugInfo,
_DirectRenderer?: api.DirectRenderer,
TemplateRef_: typeof template_ref.TemplateRef_, _TemplateRef_?: template_ref.TemplateRef_<any>,
ReflectionCapabilities: typeof reflection_capabilities.ReflectionCapabilities,
_ReflectionCapabilities?: reflection_capabilities.ReflectionCapabilities,

View File

@ -89,6 +89,7 @@ export class DebugDomRenderer implements Renderer {
}
destroyView(hostElement: any, viewAllNodes: any[]) {
viewAllNodes = viewAllNodes || [];
viewAllNodes.forEach((node) => { removeDebugNodeFromIndex(getDebugNode(node)); });
this._delegate.destroyView(hostElement, viewAllNodes);
}

View File

@ -11,7 +11,7 @@ import {Injector, THROW_IF_NOT_FOUND} from '../di/injector';
import {ListWrapper} from '../facade/collection';
import {isPresent} from '../facade/lang';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from '../profile/profile';
import {RenderComponentType, RenderDebugInfo, Renderer} from '../render/api';
import {DirectRenderer, RenderComponentType, RenderDebugInfo, Renderer} from '../render/api';
import {AnimationViewContext} from './animation_view_context';
import {ComponentRef} from './component_factory';
@ -51,6 +51,7 @@ export abstract class AppView<T> {
private _hostInjector: Injector;
private _hostProjectableNodes: any[][];
private _animationContext: AnimationViewContext;
private _directRenderer: DirectRenderer;
public context: T;
@ -64,6 +65,7 @@ export abstract class AppView<T> {
} else {
this.renderer = parentView.renderer;
}
this._directRenderer = (this.renderer as any).directRenderer;
}
get animationContext(): AnimationViewContext {
@ -136,7 +138,7 @@ export abstract class AppView<T> {
detachAndDestroy() {
if (this._hasExternalHostElement) {
this.renderer.detachView(this.flatRootNodes);
this.detach();
} else if (isPresent(this.viewContainerElement)) {
this.viewContainerElement.detachView(this.viewContainerElement.nestedViews.indexOf(this));
}
@ -179,13 +181,34 @@ export abstract class AppView<T> {
detach(): void {
this.detachInternal();
if (this._animationContext) {
this._animationContext.onAllActiveAnimationsDone(
() => this.renderer.detachView(this.flatRootNodes));
this._animationContext.onAllActiveAnimationsDone(() => this._renderDetach());
} else {
this._renderDetach();
}
}
private _renderDetach() {
if (this._directRenderer) {
this.visitRootNodesInternal(this._directRenderer.remove, null);
} else {
this.renderer.detachView(this.flatRootNodes);
}
}
attachAfter(prevNode: any) {
if (this._directRenderer) {
const nextSibling = this._directRenderer.nextSibling(prevNode);
if (nextSibling) {
this.visitRootNodesInternal(this._directRenderer.insertBefore, nextSibling);
} else {
this.visitRootNodesInternal(
this._directRenderer.appendChild, this._directRenderer.parentElement(prevNode));
}
} else {
this.renderer.attachViewAfter(prevNode, this.flatRootNodes);
}
}
get changeDetectorRef(): ChangeDetectorRef { return this.ref; }
get flatRootNodes(): any[] {
@ -194,10 +217,14 @@ export abstract class AppView<T> {
return nodes;
}
projectedNodes(ngContentIndex: number): any[] {
const nodes: any[] = [];
this.visitProjectedNodes(ngContentIndex, addToArray, nodes);
return nodes;
projectNodes(parentElement: any, ngContentIndex: number) {
if (this._directRenderer) {
this.visitProjectedNodes(ngContentIndex, this._directRenderer.appendChild, parentElement);
} else {
const nodes: any[] = [];
this.visitProjectedNodes(ngContentIndex, addToArray, nodes);
this.renderer.projectNodes(parentElement, nodes);
}
}
visitProjectedNodes<C>(ngContentIndex: number, cb: (node: any, ctx: C) => void, c: C): void {

View File

@ -90,7 +90,7 @@ export class ViewContainer {
refRenderNode = this.nativeElement;
}
if (isPresent(refRenderNode)) {
view.renderer.attachViewAfter(refRenderNode, view.flatRootNodes);
view.attachAfter(refRenderNode);
}
view.markContentChildAsMoved(this);
}
@ -118,7 +118,7 @@ export class ViewContainer {
refRenderNode = this.nativeElement;
}
if (isPresent(refRenderNode)) {
view.renderer.attachViewAfter(refRenderNode, view.flatRootNodes);
view.attachAfter(refRenderNode);
}
view.addToContentChildren(this);
}

View File

@ -33,6 +33,14 @@ export abstract class RenderDebugInfo {
get source(): string { return unimplemented(); }
}
export interface DirectRenderer {
remove(node: any): void;
appendChild(node: any, parent: any): void;
insertBefore(node: any, refNode: any): void;
nextSibling(node: any): any;
parentElement(node: any): any;
}
/**
* @experimental
*/

View File

@ -0,0 +1,144 @@
/**
* @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 {Component, Injectable, RenderComponentType, Renderer, RootRenderer} from '@angular/core';
import {DebugDomRenderer} from '@angular/core/src/debug/debug_renderer';
import {DirectRenderer} from '@angular/core/src/render/api';
import {TestBed, inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {DIRECT_DOM_RENDERER, DomRootRenderer} from '@angular/platform-browser/src/dom/dom_renderer';
import {expect} from '@angular/platform-browser/testing/matchers';
let directRenderer: any;
let destroyViewLogs: any[];
export function main() {
// Don't run on server...
if (!getDOM().supportsDOMEvents()) return;
describe('direct dom integration tests', function() {
beforeEach(() => {
directRenderer = DIRECT_DOM_RENDERER;
destroyViewLogs = [];
spyOn(directRenderer, 'remove').and.callThrough();
spyOn(directRenderer, 'appendChild').and.callThrough();
spyOn(directRenderer, 'insertBefore').and.callThrough();
TestBed.configureTestingModule(
{providers: [{provide: RootRenderer, useClass: DirectRootRenderer}]});
});
it('should attach views as last nodes in a parent', () => {
@Component({template: '<div *ngIf="true">hello</div>'})
class MyComp {
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
directRenderer.insertBefore.calls.reset();
directRenderer.appendChild.calls.reset();
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('hello');
expect(directRenderer.insertBefore).not.toHaveBeenCalled();
expect(directRenderer.appendChild).toHaveBeenCalled();
});
it('should attach views as non last nodes in a parent', () => {
@Component({template: '<div *ngIf="true">hello</div>after'})
class MyComp {
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
directRenderer.insertBefore.calls.reset();
directRenderer.appendChild.calls.reset();
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('helloafter');
expect(directRenderer.insertBefore).toHaveBeenCalled();
expect(directRenderer.appendChild).not.toHaveBeenCalled();
});
it('should detach views', () => {
@Component({template: '<div *ngIf="shown">hello</div>'})
class MyComp {
shown = true;
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
directRenderer.remove.calls.reset();
fixture.componentInstance.shown = false;
fixture.detectChanges();
expect(fixture.nativeElement).toHaveText('');
expect(directRenderer.remove).toHaveBeenCalled();
});
it('should pass null as all nodes to destroyView', () => {
@Component({template: '<div *ngIf="shown">hello</div>'})
class MyComp {
shown = true;
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
destroyViewLogs.splice(0, destroyViewLogs.length);
fixture.componentInstance.shown = false;
fixture.detectChanges();
expect(destroyViewLogs).toEqual([[null, null]]);
});
it('should project nodes', () => {
@Component({template: '<child>hello</child>'})
class Parent {
}
@Component({selector: 'child', template: '(<ng-content></ng-content>)'})
class Child {
}
TestBed.configureTestingModule({declarations: [Parent, Child]});
const fixture = TestBed.createComponent(Parent);
expect(fixture.nativeElement).toHaveText('(hello)');
const childHostEl = fixture.nativeElement.children[0];
const projectedNode = childHostEl.childNodes[1];
expect(directRenderer.appendChild).toHaveBeenCalledWith(projectedNode, childHostEl);
});
});
}
@Injectable()
class DirectRootRenderer implements RootRenderer {
constructor(private _delegate: DomRootRenderer) {}
renderComponent(componentType: RenderComponentType): Renderer {
const renderer = new DebugDomRenderer(this._delegate.renderComponent(componentType));
(renderer as any).directRenderer = directRenderer;
const originalDestroyView = renderer.destroyView;
renderer.destroyView = function(...args: any[]) {
destroyViewLogs.push(args);
return originalDestroyView.apply(this, args);
};
return renderer;
}
}

View File

@ -9,7 +9,7 @@
import {APP_ID, Inject, Injectable, RenderComponentType, Renderer, RootRenderer, ViewEncapsulation} from '@angular/core';
import {isBlank, isPresent, stringify} from '../facade/lang';
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from '../private_import_core';
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, DirectRenderer, RenderDebugInfo} from '../private_import_core';
import {AnimationDriver} from './animation_driver';
import {DOCUMENT} from './dom_tokens';
@ -54,11 +54,21 @@ export class DomRootRenderer_ extends DomRootRenderer {
}
}
export const DIRECT_DOM_RENDERER: DirectRenderer = {
remove(node: Text | Comment | Element) { node.remove();},
appendChild(node: Node, parent: Element) { parent.appendChild(node);},
insertBefore(node: Node, refNode: Node) { refNode.parentElement.insertBefore(node, refNode);},
nextSibling(node: Node) { return node.nextSibling;},
parentElement(node: Node): Element{return node.parentElement;}
};
export class DomRenderer implements Renderer {
private _contentAttr: string;
private _hostAttr: string;
private _styles: string[];
directRenderer: DirectRenderer = DIRECT_DOM_RENDERER;
constructor(
private _rootRenderer: DomRootRenderer, private componentProto: RenderComponentType,
private _animationDriver: AnimationDriver, styleShimId: string) {

View File

@ -10,6 +10,7 @@ import {__core_private__ as r} from '@angular/core';
export type RenderDebugInfo = typeof r._RenderDebugInfo;
export var RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo;
export type DirectRenderer = typeof r._DirectRenderer;
export var ReflectionCapabilities: typeof r.ReflectionCapabilities = r.ReflectionCapabilities;