fix(core): Call `onDestroy` in production mode as well (#40120)
PR #39876 introduced an error where the `onDestroy` of `ComponentRef` would only get called if `ngDevMode` was set to true. This was because in dev mode we would freeze `TCleanup` to verify that no more static cleanup would get added to `TCleanup` array. This ensured that `TCleanup` was always present in dev mode. In production the `TCleanup` would get created only when needed. The resulting cleanup code was incorrectly indented and would only run if `TCleanup` was present causing this issue. Fix #40105 PR Close #40120
This commit is contained in:
parent
5ba7bcd2a6
commit
fc1cd07eb0
|
@ -19,7 +19,7 @@ import {assertTNodeType} from '../node_assert';
|
||||||
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
|
import {getCurrentDirectiveDef, getCurrentTNode, getLView, getTView} from '../state';
|
||||||
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
|
import {getComponentLViewByIndex, getNativeByTNode, unwrapRNode} from '../util/view_utils';
|
||||||
|
|
||||||
import {getLCleanup, getTViewCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
|
import {getOrCreateLViewCleanup, getOrCreateTViewCleanup, handleError, loadComponentRenderer, markViewDirty} from './shared';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,12 +120,12 @@ function listenerInternal(
|
||||||
eventTargetResolver?: GlobalTargetResolver): void {
|
eventTargetResolver?: GlobalTargetResolver): void {
|
||||||
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
const isTNodeDirectiveHost = isDirectiveHost(tNode);
|
||||||
const firstCreatePass = tView.firstCreatePass;
|
const firstCreatePass = tView.firstCreatePass;
|
||||||
const tCleanup: false|any[] = firstCreatePass && getTViewCleanup(tView);
|
const tCleanup: false|any[] = firstCreatePass && getOrCreateTViewCleanup(tView);
|
||||||
|
|
||||||
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
|
// When the ɵɵlistener instruction was generated and is executed we know that there is either a
|
||||||
// native listener or a directive output on this element. As such we we know that we will have to
|
// native listener or a directive output on this element. As such we we know that we will have to
|
||||||
// register a listener and store its cleanup function on LView.
|
// register a listener and store its cleanup function on LView.
|
||||||
const lCleanup = getLCleanup(lView);
|
const lCleanup = getOrCreateLViewCleanup(lView);
|
||||||
|
|
||||||
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);
|
ngDevMode && assertTNodeType(tNode, TNodeType.AnyRNode | TNodeType.AnyContainer);
|
||||||
|
|
||||||
|
|
|
@ -759,19 +759,19 @@ export function locateHostElement(
|
||||||
*/
|
*/
|
||||||
export function storeCleanupWithContext(
|
export function storeCleanupWithContext(
|
||||||
tView: TView, lView: LView, context: any, cleanupFn: Function): void {
|
tView: TView, lView: LView, context: any, cleanupFn: Function): void {
|
||||||
const lCleanup = getLCleanup(lView);
|
const lCleanup = getOrCreateLViewCleanup(lView);
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
// If context is null that this is instance specific callback. These callbacks can only be
|
// If context is null that this is instance specific callback. These callbacks can only be
|
||||||
// inserted after template shared instances. For this reason in ngDevMode we freeze the TView.
|
// inserted after template shared instances. For this reason in ngDevMode we freeze the TView.
|
||||||
if (ngDevMode) {
|
if (ngDevMode) {
|
||||||
Object.freeze(getTViewCleanup(tView));
|
Object.freeze(getOrCreateTViewCleanup(tView));
|
||||||
}
|
}
|
||||||
lCleanup.push(cleanupFn);
|
lCleanup.push(cleanupFn);
|
||||||
} else {
|
} else {
|
||||||
lCleanup.push(context);
|
lCleanup.push(context);
|
||||||
|
|
||||||
if (tView.firstCreatePass) {
|
if (tView.firstCreatePass) {
|
||||||
getTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1);
|
getOrCreateTViewCleanup(tView).push(cleanupFn, lCleanup.length - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2006,12 +2006,12 @@ export function storePropertyBindingMetadata(
|
||||||
|
|
||||||
export const CLEAN_PROMISE = _CLEAN_PROMISE;
|
export const CLEAN_PROMISE = _CLEAN_PROMISE;
|
||||||
|
|
||||||
export function getLCleanup(view: LView): any[] {
|
export function getOrCreateLViewCleanup(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() : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTViewCleanup(tView: TView): any[] {
|
export function getOrCreateTViewCleanup(tView: TView): any[] {
|
||||||
return tView.cleanup || (tView.cleanup = ngDevMode ? new TCleanup() : []);
|
return tView.cleanup || (tView.cleanup = ngDevMode ? new TCleanup() : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -480,12 +480,12 @@ function processCleanups(tView: TView, lView: LView): void {
|
||||||
tCleanup[i].call(context);
|
tCleanup[i].call(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lCleanup !== null) {
|
}
|
||||||
for (let i = lastLCleanupIndex + 1; i < lCleanup.length; i++) {
|
if (lCleanup !== null) {
|
||||||
const instanceCleanupFn = lCleanup[i];
|
for (let i = lastLCleanupIndex + 1; i < lCleanup.length; i++) {
|
||||||
ngDevMode && assertFunction(instanceCleanupFn, 'Expecting instance cleanup function.');
|
const instanceCleanupFn = lCleanup[i];
|
||||||
instanceCleanupFn();
|
ngDevMode && assertFunction(instanceCleanupFn, 'Expecting instance cleanup function.');
|
||||||
}
|
instanceCleanupFn();
|
||||||
}
|
}
|
||||||
lView[CLEANUP] = null;
|
lView[CLEANUP] = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -303,55 +303,71 @@ describe('component', () => {
|
||||||
expect(wrapperEls.length).toBe(2); // other elements are preserved
|
expect(wrapperEls.length).toBe(2); // other elements are preserved
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should invoke `onDestroy` callbacks of dynamically created component', () => {
|
describe('with ngDevMode', () => {
|
||||||
let wasOnDestroyCalled = false;
|
const _global: {ngDevMode: any} = global as any;
|
||||||
@Component({
|
let saveNgDevMode!: typeof ngDevMode;
|
||||||
selector: '[comp]',
|
beforeEach(() => saveNgDevMode = ngDevMode);
|
||||||
template: 'comp content',
|
afterEach(() => _global.ngDevMode = saveNgDevMode);
|
||||||
})
|
// In dev mode we have some additional logic to freeze `TView.cleanup` array
|
||||||
class DynamicComponent {
|
// (see `storeCleanupWithContext` function).
|
||||||
}
|
// The tests below verify that this action doesn't trigger any change in behaviour
|
||||||
|
// for prod mode. See https://github.com/angular/angular/issues/40105.
|
||||||
|
['ngDevMode off', 'ngDevMode on'].forEach((mode) => {
|
||||||
|
it('should invoke `onDestroy` callbacks of dynamically created component with ' + mode,
|
||||||
|
() => {
|
||||||
|
if (mode === 'ngDevMode off') {
|
||||||
|
_global.ngDevMode = false;
|
||||||
|
}
|
||||||
|
let wasOnDestroyCalled = false;
|
||||||
|
@Component({
|
||||||
|
selector: '[comp]',
|
||||||
|
template: 'comp content',
|
||||||
|
})
|
||||||
|
class DynamicComponent {
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [DynamicComponent],
|
declarations: [DynamicComponent],
|
||||||
entryComponents: [DynamicComponent], // needed only for ViewEngine
|
entryComponents: [DynamicComponent], // needed only for ViewEngine
|
||||||
})
|
})
|
||||||
class TestModule {
|
class TestModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'button',
|
selector: 'button',
|
||||||
template: '<div id="app-root" #anchor></div>',
|
template: '<div id="app-root" #anchor></div>',
|
||||||
})
|
})
|
||||||
class App {
|
class App {
|
||||||
@ViewChild('anchor', {read: ViewContainerRef}) anchor!: ViewContainerRef;
|
@ViewChild('anchor', {read: ViewContainerRef}) anchor!: ViewContainerRef;
|
||||||
|
|
||||||
constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {}
|
constructor(private cfr: ComponentFactoryResolver, private injector: Injector) {}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
const factory = this.cfr.resolveComponentFactory(DynamicComponent);
|
const factory = this.cfr.resolveComponentFactory(DynamicComponent);
|
||||||
const componentRef = factory.create(this.injector);
|
const componentRef = factory.create(this.injector);
|
||||||
componentRef.onDestroy(() => {
|
componentRef.onDestroy(() => {
|
||||||
wasOnDestroyCalled = true;
|
wasOnDestroyCalled = true;
|
||||||
});
|
});
|
||||||
this.anchor.insert(componentRef.hostView);
|
this.anchor.insert(componentRef.hostView);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.anchor.clear();
|
this.anchor.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TestBed.configureTestingModule({imports: [TestModule], declarations: [App]});
|
TestBed.configureTestingModule({imports: [TestModule], declarations: [App]});
|
||||||
const fixture = TestBed.createComponent(App);
|
const fixture = TestBed.createComponent(App);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
// Add ComponentRef to ViewContainerRef instance.
|
// Add ComponentRef to ViewContainerRef instance.
|
||||||
fixture.componentInstance.create();
|
fixture.componentInstance.create();
|
||||||
// Clear ViewContainerRef to invoke `onDestroy` callbacks on ComponentRef.
|
// Clear ViewContainerRef to invoke `onDestroy` callbacks on ComponentRef.
|
||||||
fixture.componentInstance.clear();
|
fixture.componentInstance.clear();
|
||||||
|
|
||||||
expect(wasOnDestroyCalled).toBeTrue();
|
expect(wasOnDestroyCalled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid host element', () => {
|
describe('invalid host element', () => {
|
||||||
|
|
|
@ -1019,9 +1019,6 @@
|
||||||
{
|
{
|
||||||
"name": "getInjectorIndex"
|
"name": "getInjectorIndex"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "getLCleanup"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getLView"
|
"name": "getLView"
|
||||||
},
|
},
|
||||||
|
@ -1052,6 +1049,9 @@
|
||||||
{
|
{
|
||||||
"name": "getOrCreateInjectable"
|
"name": "getOrCreateInjectable"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getOrCreateLViewCleanup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getOrCreateNodeInjectorForNode"
|
"name": "getOrCreateNodeInjectorForNode"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1334,9 +1334,6 @@
|
||||||
{
|
{
|
||||||
"name": "getInjectorIndex"
|
"name": "getInjectorIndex"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "getLCleanup"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getLView"
|
"name": "getLView"
|
||||||
},
|
},
|
||||||
|
@ -1367,6 +1364,9 @@
|
||||||
{
|
{
|
||||||
"name": "getOrCreateInjectable"
|
"name": "getOrCreateInjectable"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getOrCreateLViewCleanup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getOrCreateNodeInjectorForNode"
|
"name": "getOrCreateNodeInjectorForNode"
|
||||||
},
|
},
|
||||||
|
@ -1376,6 +1376,9 @@
|
||||||
{
|
{
|
||||||
"name": "getOrCreateTNode"
|
"name": "getOrCreateTNode"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getOrCreateTViewCleanup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getOrCreateViewRefs"
|
"name": "getOrCreateViewRefs"
|
||||||
},
|
},
|
||||||
|
@ -1442,9 +1445,6 @@
|
||||||
{
|
{
|
||||||
"name": "getTView"
|
"name": "getTView"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "getTViewCleanup"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getToken"
|
"name": "getToken"
|
||||||
},
|
},
|
||||||
|
|
|
@ -377,9 +377,6 @@
|
||||||
{
|
{
|
||||||
"name": "getInjectorIndex"
|
"name": "getInjectorIndex"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "getLCleanup"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getLView"
|
"name": "getLView"
|
||||||
},
|
},
|
||||||
|
@ -404,6 +401,9 @@
|
||||||
{
|
{
|
||||||
"name": "getOrCreateInjectable"
|
"name": "getOrCreateInjectable"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "getOrCreateLViewCleanup"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "getOrCreateNodeInjectorForNode"
|
"name": "getOrCreateNodeInjectorForNode"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue