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) {
throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?');
}
moduleRef.onDestroy(() => remove(this._modules, moduleRef));
ngZone!.runOutsideAngular(() => ngZone!.onError.subscribe({
next: (error: any) => {
exceptionHandler.handleError(error);
}
}));
ngZone!.runOutsideAngular(() => {
const subscription = ngZone!.onError.subscribe({
next: (error: any) => {
exceptionHandler.handleError(error);
}
});
moduleRef.onDestroy(() => {
remove(this._modules, moduleRef);
subscription.unsubscribe();
});
});
return _callAndReportToErrorHandler(exceptionHandler, ngZone!, () => {
const initStatus: ApplicationInitStatus = moduleRef.injector.get(ApplicationInitStatus);
initStatus.runInitializers();

View File

@ -6,7 +6,7 @@
* 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 {BrowserModule} from '@angular/platform-browser';
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', () => {
beforeEach(() => {
spyOn(console, 'error');
@ -365,4 +381,4 @@ export class MultipleSelectorsAppComponent {
bootstrap: [MultipleSelectorsAppComponent],
})
export class MultipleSelectorsAppModule {
}
}