fix(core): unsubscribe from the `onError` when the root view is removed (#39940)

At the moment, when creating a root module, a subscription to the
`onError` subject is also created. It captures the scope where `NgModuleRef`
is created and prevents it from being garbage collected. Also note that this
`NgModuleRef` has a reference to the root module instance (e.g. `AppModule`),
which also prevents it from being GC'd.

PR Close #39940
This commit is contained in:
arturovt 2020-12-03 01:24:29 +02:00 committed by Misko Hevery
parent f01c713ee2
commit 5a3a154cd8
2 changed files with 29 additions and 8 deletions

View File

@ -350,12 +350,17 @@ export class PlatformRef {
if (!exceptionHandler) { if (!exceptionHandler) {
throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?'); throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
} }
moduleRef.onDestroy(() => remove(this._modules, moduleRef)); ngZone!.runOutsideAngular(() => {
ngZone!.runOutsideAngular(() => ngZone!.onError.subscribe({ const subscription = ngZone!.onError.subscribe({
next: (error: any) => { next: (error: any) => {
exceptionHandler.handleError(error); exceptionHandler.handleError(error);
} }
})); });
moduleRef.onDestroy(() => {
remove(this._modules, moduleRef);
subscription.unsubscribe();
});
});
return _callAndReportToErrorHandler(exceptionHandler, ngZone!, () => { return _callAndReportToErrorHandler(exceptionHandler, ngZone!, () => {
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus); const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
initStatus.runInitializers(); initStatus.runInitializers();

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, TestabilityRegistry, ViewEncapsulation} from '@angular/core'; import {ApplicationRef, COMPILER_OPTIONS, Component, destroyPlatform, NgModule, NgZone, TestabilityRegistry, ViewEncapsulation} from '@angular/core';
import {expect} from '@angular/core/testing/src/testing_internal'; import {expect} from '@angular/core/testing/src/testing_internal';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -227,6 +227,22 @@ describe('bootstrap', () => {
})); }));
}); });
describe('PlatformRef cleanup', () => {
it('should unsubscribe from `onError` when Injector is destroyed',
withBody('<my-app></my-app>', async () => {
const TestModule = createComponentAndModule();
const ngModuleRef = await platformBrowserDynamic().bootstrapModule(TestModule);
const ngZone = ngModuleRef.injector.get(NgZone);
expect(ngZone.onError.observers.length).toBe(1);
ngModuleRef.destroy();
expect(ngZone.onError.observers.length).toBe(0);
}));
});
onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => { onlyInIvy('options cannot be changed in Ivy').describe('changing bootstrap options', () => {
beforeEach(() => { beforeEach(() => {
spyOn(console, 'error'); spyOn(console, 'error');
@ -365,4 +381,4 @@ export class MultipleSelectorsAppComponent {
bootstrap: [MultipleSelectorsAppComponent], bootstrap: [MultipleSelectorsAppComponent],
}) })
export class MultipleSelectorsAppModule { export class MultipleSelectorsAppModule {
} }