feat(ivy): support renderer.destroy and renderer.destroyNode hooks (#24049)

PR Close #24049
This commit is contained in:
Marc Laval 2018-05-22 17:40:59 +02:00 committed by Victor Berchet
parent f6f44edcc0
commit 2e21690c66
5 changed files with 162 additions and 12 deletions

View File

@ -228,10 +228,10 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
### View Encapsulation ### View Encapsulation
| Feature | Runtime | Spec | Compiler | | Feature | Runtime | Spec | Compiler |
| ----------------------------------- | ------- | -------- | -------- | | ----------------------------------- | ------- | -------- | -------- |
| Render3.None | ✅ | ✅ | ✅ | | Renderer3.None | ✅ | ✅ | ✅ |
| Render2.None | ✅ | ✅ | ✅ | | Renderer2.None | ✅ | ✅ | ✅ |
| Render2.Emulated | ❌ | ❌ | ❌ | | Renderer2.Emulated | ❌ | ❌ | ❌ |
| Render2.Native | ❌ | ❌ | ❌ | | Renderer2.Native | ❌ | ❌ | ❌ |
@ -254,3 +254,28 @@ The goal is for the `@Component` (and friends) to be the compiler of template. S
| `checkNoChanges()` | n/a | n/a | ❌ | n/a | n/a | ✅ | | `checkNoChanges()` | n/a | n/a | ❌ | n/a | n/a | ✅ |
| `reattach()` | n/a | n/a | ❌ | n/a | n/a | ✅ | | `reattach()` | n/a | n/a | ❌ | n/a | n/a | ✅ |
| `nativeElement()` | n/a | n/a | n/a | n/a | ✅ | n/a | | `nativeElement()` | n/a | n/a | n/a | n/a | ✅ | n/a |
### Renderer2
| Method | Runtime |
| ----------------------------------- | ------- |
| `data()` | n/a |
| `destroy()` | ✅ |
| `createElement()` | ✅ |
| `createComment()` | n/a |
| `createText()` | ✅ |
| `destroyNode()` | ✅ |
| `appendChild()` | ✅ |
| `insertBefore()` | ✅ |
| `removeChild()` | ✅ |
| `selectRootElement()` | ✅ |
| `parentNode()` | ❌ |
| `nextSibling()` | ❌ |
| `setAttribute()` | ✅ |
| `removeAttribute()` | ✅ |
| `addClass()` | ✅ |
| `removeClass()` | ✅ |
| `setStyle()` | ✅ |
| `removeStyle()` | ✅ |
| `setProperty()` | ✅ |
| `setValue()` | ✅ |
| `listen()` | ✅ |

View File

@ -25,6 +25,8 @@ declare global {
rendererRemoveClass: number; rendererRemoveClass: number;
rendererSetStyle: number; rendererSetStyle: number;
rendererRemoveStyle: number; rendererRemoveStyle: number;
rendererDestroy: number;
rendererDestroyNode: number;
} }
} }
@ -50,6 +52,8 @@ export const ngDevModeResetPerfCounters: () => void =
rendererRemoveClass: 0, rendererRemoveClass: 0,
rendererSetStyle: 0, rendererSetStyle: 0,
rendererRemoveStyle: 0, rendererRemoveStyle: 0,
rendererDestroy: 0,
rendererDestroyNode: 0,
}; };
} }
ngDevModeResetPerfCounters(); ngDevModeResetPerfCounters();

View File

@ -9,7 +9,7 @@
import {assertNotNull} from './assert'; import {assertNotNull} from './assert';
import {callHooks} from './hooks'; import {callHooks} from './hooks';
import {LContainer, unusedValueExportToPlacateAjd as unused1} from './interfaces/container'; import {LContainer, unusedValueExportToPlacateAjd as unused1} from './interfaces/container';
import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node'; import {LContainerNode, LElementNode, LNode, LProjectionNode, LTextNode, LViewNode, TNodeFlags, TNodeType, unusedValueExportToPlacateAjd as unused2} from './interfaces/node';
import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection'; import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection';
import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {ProceduralRenderer3, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer';
import {HookData, LView, LViewOrLContainer, TView, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {HookData, LView, LViewOrLContainer, TView, unusedValueExportToPlacateAjd as unused5} from './interfaces/view';
@ -215,9 +215,16 @@ export function addRemoveViewFromContainer(
renderer.insertBefore(parent, node.native !, beforeNode as RNode | null) : renderer.insertBefore(parent, node.native !, beforeNode as RNode | null) :
parent.insertBefore(node.native !, beforeNode as RNode | null, true); parent.insertBefore(node.native !, beforeNode as RNode | null, true);
} else { } else {
isProceduralRenderer(renderer) ? renderer.removeChild(parent as RElement, node.native !) : if (isProceduralRenderer(renderer)) {
renderer.removeChild(parent as RElement, node.native !);
if (renderer.destroyNode) {
ngDevMode && ngDevMode.rendererDestroyNode++;
renderer.destroyNode(node.native !);
}
} else {
parent.removeChild(node.native !); parent.removeChild(node.native !);
} }
}
nextNode = getNextLNode(node); nextNode = getNextLNode(node);
} else if (node.tNode.type === TNodeType.Container) { } else if (node.tNode.type === TNodeType.Container) {
// if we get to a container, it must be a root node of a view because we are only // if we get to a container, it must be a root node of a view because we are only
@ -398,6 +405,11 @@ function cleanUpView(view: LView): void {
removeListeners(view); removeListeners(view);
executeOnDestroys(view); executeOnDestroys(view);
executePipeOnDestroys(view); executePipeOnDestroys(view);
// For component views only, the local renderer is destroyed as clean up time.
if (view.id === -1 && isProceduralRenderer(view.renderer)) {
ngDevMode && ngDevMode.rendererDestroy++;
view.renderer.destroy();
}
} }
/** Removes listeners and unsubscribes from output subscriptions */ /** Removes listeners and unsubscribes from output subscriptions */

View File

@ -53,6 +53,7 @@ export class TemplateFixture extends BaseFixture {
private _directiveDefs: DirectiveDefList|null; private _directiveDefs: DirectiveDefList|null;
private _pipeDefs: PipeDefList|null; private _pipeDefs: PipeDefList|null;
private _sanitizer: Sanitizer|null; private _sanitizer: Sanitizer|null;
private _rendererFactory: RendererFactory3;
/** /**
* *
@ -64,11 +65,12 @@ export class TemplateFixture extends BaseFixture {
constructor( constructor(
private createBlock: () => void, private updateBlock: () => void = noop, private createBlock: () => void, private updateBlock: () => void = noop,
directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null, directives?: DirectiveTypesOrFactory|null, pipes?: PipeTypesOrFactory|null,
sanitizer?: Sanitizer) { sanitizer?: Sanitizer|null, rendererFactory?: RendererFactory3) {
super(); super();
this._directiveDefs = toDefs(directives, extractDirectiveDef); this._directiveDefs = toDefs(directives, extractDirectiveDef);
this._pipeDefs = toDefs(pipes, extractPipeDef); this._pipeDefs = toDefs(pipes, extractPipeDef);
this._sanitizer = sanitizer || null; this._sanitizer = sanitizer || null;
this._rendererFactory = rendererFactory || domRendererFactory3;
this.hostNode = renderTemplate(this.hostElement, (rf: RenderFlags, ctx: any) => { this.hostNode = renderTemplate(this.hostElement, (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
this.createBlock(); this.createBlock();
@ -76,7 +78,7 @@ export class TemplateFixture extends BaseFixture {
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
this.updateBlock(); this.updateBlock();
} }
}, null !, domRendererFactory3, null, this._directiveDefs, this._pipeDefs, sanitizer); }, null !, this._rendererFactory, null, this._directiveDefs, this._pipeDefs, sanitizer);
} }
/** /**
@ -86,7 +88,7 @@ export class TemplateFixture extends BaseFixture {
*/ */
update(updateBlock?: () => void): void { update(updateBlock?: () => void): void {
renderTemplate( renderTemplate(
this.hostNode.native, updateBlock || this.updateBlock, null !, domRendererFactory3, this.hostNode.native, updateBlock || this.updateBlock, null !, this._rendererFactory,
this.hostNode, this._directiveDefs, this._pipeDefs, this._sanitizer); this.hostNode, this._directiveDefs, this._pipeDefs, this._sanitizer);
} }
} }

View File

@ -11,12 +11,12 @@ import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/brow
import {RendererType2, ViewEncapsulation} from '../../src/core'; import {RendererType2, ViewEncapsulation} from '../../src/core';
import {defineComponent, detectChanges} from '../../src/render3/index'; import {defineComponent, detectChanges} from '../../src/render3/index';
import {bind, elementEnd, elementProperty, elementStart, listener, text, tick} from '../../src/render3/instructions'; import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, listener, text, tick} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition'; import {RenderFlags} from '../../src/render3/interfaces/definition';
import {createRendererType2} from '../../src/view/index'; import {createRendererType2} from '../../src/view/index';
import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2'; import {getAnimationRendererFactory2, getRendererFactory2} from './imported_renderer2';
import {containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util'; import {TemplateFixture, containerEl, document, renderComponent, renderToHtml, toHtml} from './render_util';
describe('renderer factory lifecycle', () => { describe('renderer factory lifecycle', () => {
let logs: string[] = []; let logs: string[] = [];
@ -206,3 +206,110 @@ describe('animation renderer factory', () => {
}); });
}); });
}); });
describe('Renderer2 destruction hooks', () => {
const rendererFactory = getRendererFactory2(document);
const origCreateRenderer = rendererFactory.createRenderer;
rendererFactory.createRenderer = function() {
const renderer = origCreateRenderer.apply(this, arguments);
renderer.destroyNode = () => {};
return renderer;
};
it('should call renderer.destroyNode for each node destroyed', () => {
let condition = true;
function createTemplate() {
elementStart(0, 'div');
{ container(1); }
elementEnd();
}
function updateTemplate() {
containerRefreshStart(1);
{
if (condition) {
let rf1 = embeddedViewStart(1);
{
if (rf1 & RenderFlags.Create) {
elementStart(0, 'span');
elementEnd();
elementStart(1, 'span');
elementEnd();
elementStart(2, 'span');
elementEnd();
}
}
embeddedViewEnd();
}
}
containerRefreshEnd();
}
const t =
new TemplateFixture(createTemplate, updateTemplate, null, null, null, rendererFactory);
expect(t.html).toEqual('<div><span></span><span></span><span></span></div>');
condition = false;
t.update();
expect(t.html).toEqual('<div></div>');
expect(ngDevMode).toHaveProperties({rendererDestroy: 0, rendererDestroyNode: 3});
});
it('should call renderer.destroy for each component destroyed', () => {
class SimpleComponent {
static ngComponentDef = defineComponent({
type: SimpleComponent,
selectors: [['simple']],
template: function(rf: RenderFlags, ctx: SimpleComponent) {
if (rf & RenderFlags.Create) {
elementStart(0, 'span');
elementEnd();
}
},
factory: () => new SimpleComponent,
});
}
let condition = true;
function createTemplate() {
elementStart(0, 'div');
{ container(1); }
elementEnd();
}
function updateTemplate() {
containerRefreshStart(1);
{
if (condition) {
let rf1 = embeddedViewStart(1);
{
if (rf1 & RenderFlags.Create) {
elementStart(0, 'simple');
elementEnd();
elementStart(1, 'span');
elementEnd();
elementStart(2, 'simple');
elementEnd();
}
}
embeddedViewEnd();
}
}
containerRefreshEnd();
}
const t = new TemplateFixture(
createTemplate, updateTemplate, [SimpleComponent], null, null, rendererFactory);
expect(t.html).toEqual(
'<div><simple><span></span></simple><span></span><simple><span></span></simple></div>');
condition = false;
t.update();
expect(t.html).toEqual('<div></div>');
expect(ngDevMode).toHaveProperties({rendererDestroy: 2, rendererDestroyNode: 3});
});
});