Skip to content

Commit

Permalink
refactor(core): move Zone providers to a single provider function (#4…
Browse files Browse the repository at this point in the history
…9373)

This commit moves the providers for `NgZone`-based change detection to a
single provider function. This function is currently called by default
in all places where `NgZone` was provided
(`bootstrapApplication`, `bootstrapModule`, and `TestBed`).

When we want to make Angular applications zoneless by default, we
can make a public provider method that has to be used in order to enable
the zone change detection features. When this method is not called,
Angular would use `NoopNgZone` by default and not initialize any
subscriptions to the `NgZone` stability events.

Side note: There are actually two places that `NgZone` is provided for `TestBed`
(providers in `compileTestModule` and `BrowserTestingModule`). This
likely doesn't need to be in both locations.

PR Close #49373
  • Loading branch information
atscott authored and alxhub committed Mar 14, 2023
1 parent 67c5272 commit 4e098fa
Show file tree
Hide file tree
Showing 20 changed files with 281 additions and 108 deletions.
4 changes: 2 additions & 2 deletions goldens/public-api/core/errors.md
Expand Up @@ -27,8 +27,6 @@ export const enum RuntimeErrorCode {
// (undocumented)
DUPLICATE_DIRECTITVE = 309,
// (undocumented)
ERROR_HANDLER_NOT_FOUND = 402,
// (undocumented)
EXPORT_NOT_FOUND = -301,
// (undocumented)
EXPRESSION_CHANGED_AFTER_CHECKED = -100,
Expand Down Expand Up @@ -69,6 +67,8 @@ export const enum RuntimeErrorCode {
// (undocumented)
MISSING_LOCALE_DATA = 701,
// (undocumented)
MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP = 402,
// (undocumented)
MISSING_ZONEJS = 908,
// (undocumented)
MULTIPLE_COMPONENTS_MATCH = -300,
Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/aio-payloads.json
Expand Up @@ -12,7 +12,7 @@
"aio-local": {
"uncompressed": {
"runtime": 4325,
"main": 468169,
"main": 469002,
"polyfills": 33836,
"styles": 74561,
"light-theme": 92890,
Expand Down
12 changes: 6 additions & 6 deletions goldens/size-tracking/integration-payloads.json
Expand Up @@ -2,7 +2,7 @@
"cli-hello-world": {
"uncompressed": {
"runtime": 908,
"main": 126848,
"main": 128010,
"polyfills": 33792
}
},
Expand All @@ -19,36 +19,36 @@
"cli-hello-world-ivy-i18n": {
"uncompressed": {
"runtime": 926,
"main": 125546,
"main": 126699,
"polyfills": 34676
}
},
"cli-hello-world-lazy": {
"uncompressed": {
"runtime": 2734,
"main": 230728,
"main": 231317,
"polyfills": 33810,
"src_app_lazy_lazy_routes_ts": 487
}
},
"forms": {
"uncompressed": {
"runtime": 888,
"main": 159074,
"main": 160227,
"polyfills": 33772
}
},
"animations": {
"uncompressed": {
"runtime": 898,
"main": 158262,
"main": 159415,
"polyfills": 33782
}
},
"standalone-bootstrap": {
"uncompressed": {
"runtime": 918,
"main": 86351,
"main": 86975,
"polyfills": 33802
}
},
Expand Down
81 changes: 49 additions & 32 deletions packages/core/src/application_ref.ts
Expand Up @@ -14,7 +14,7 @@ import {ApplicationInitStatus} from './application_init';
import {PLATFORM_INITIALIZER} from './application_tokens';
import {getCompilerFacade, JitCompilerUsage} from './compiler/compiler_facade';
import {Console} from './console';
import {inject} from './di';
import {ENVIRONMENT_INITIALIZER, inject} from './di';
import {Injectable} from './di/injectable';
import {InjectionToken} from './di/injection_token';
import {Injector} from './di/injector';
Expand All @@ -38,12 +38,12 @@ import {isStandalone} from './render3/definition';
import {assertStandaloneComponentType} from './render3/errors';
import {setLocaleId} from './render3/i18n/i18n_locale_id';
import {setJitOptions} from './render3/jit/jit_options';
import {createEnvironmentInjector, NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {createEnvironmentInjector, createNgModuleRefWithProviders, NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
import {TESTABILITY} from './testability/testability';
import {isPromise} from './util/lang';
import {stringify} from './util/stringify';
import {IS_STABLE, NgZone, NoopNgZone} from './zone/ng_zone';
import {isStableFactory, NgZone, NoopNgZone, ZONE_IS_STABLE_OBSERVABLE} from './zone/ng_zone';

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;

Expand Down Expand Up @@ -216,7 +216,7 @@ export function internalCreateApplication(config: {
// Create root application injector based on a set of providers configured at the platform
// bootstrap level as well as providers passed to the bootstrap call by a user.
const allAppProviders = [
{provide: NgZone, useValue: ngZone},
provideNgZoneChangeDetection(ngZone),
...(appProviders || []),
];

Expand All @@ -226,7 +226,7 @@ export function internalCreateApplication(config: {
const exceptionHandler: ErrorHandler|null = envInjector.get(ErrorHandler, null);
if (NG_DEV_MODE && !exceptionHandler) {
throw new RuntimeError(
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No `ErrorHandler` found in the Dependency Injection tree.');
}

Expand Down Expand Up @@ -448,33 +448,31 @@ export class PlatformRef {
// So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory.
const ngZone = getNgZone(options?.ngZone, getNgZoneOptions(options));
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
// Note: Create ngZoneInjector within ngZone.run so that all of the instantiated services are
// created within the Angular zone
// Do not try to replace ngZone.run with ApplicationRef#run because ApplicationRef would then be
// created outside of the Angular zone.
return ngZone.run(() => {
const ngZoneInjector = Injector.create(
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
const exceptionHandler: ErrorHandler|null = moduleRef.injector.get(ErrorHandler, null);
if (!exceptionHandler) {
const moduleRef = createNgModuleRefWithProviders(
moduleFactory.moduleType, this.injector, provideNgZoneChangeDetection(ngZone));
const exceptionHandler = moduleRef.injector.get(ErrorHandler, null);
if (NG_DEV_MODE && exceptionHandler === null) {
throw new RuntimeError(
RuntimeErrorCode.ERROR_HANDLER_NOT_FOUND,
ngDevMode && 'No ErrorHandler. Is platform module (BrowserModule) included?');
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No ErrorHandler. Is platform module (BrowserModule) included?');
}
ngZone.runOutsideAngular(() => {
const subscription = ngZone.onError.subscribe({
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);
initStatus.runInitializers();
return initStatus.donePromise.then(() => {
Expand Down Expand Up @@ -755,20 +753,16 @@ export class ApplicationRef {
/**
* Returns an Observable that indicates when the application is stable or unstable.
*/
public readonly isStable = inject(IS_STABLE);
public readonly isStable = inject(ZONE_IS_STABLE_OBSERVABLE);

private readonly _injector = inject(EnvironmentInjector);
/**
* The `EnvironmentInjector` used to create this application.
*/
get injector(): EnvironmentInjector {
return this._injector;
}

/** @internal */
constructor(private _injector: EnvironmentInjector) {
inject(NgZoneChangeDetectionScheduler).initialize();
}

/**
* Bootstrap a component onto the element identified by its selector or, optionally, to a
* specified element.
Expand Down Expand Up @@ -1111,36 +1105,37 @@ function _lastDefined<T>(args: T[]): T|undefined {
*
* `NgZone` is provided by default today so the default (and only) implementation for this
* is calling `ErrorHandler.handleError` outside of the Angular zone.
*
* TODO: When NgZone is off by default, the default behavior should be to just call
* the `ErrorHandler.handleError` directly.
*/
const INTERNAL_APPLICATION_ERROR_HANDLER =
new InjectionToken<(e: any) => void>(NG_DEV_MODE ? 'internal error handler' : '', {
providedIn: 'root',
factory: () => {
const zone = inject(NgZone);
const userErrorHandler = inject(ErrorHandler);
return (e) => zone.runOutsideAngular(() => userErrorHandler.handleError(e));
return userErrorHandler.handleError.bind(this);
}
});

function ngZoneApplicationErrorHandlerFactory() {
const zone = inject(NgZone);
const userErrorHandler = inject(ErrorHandler);
return (e: unknown) => zone.runOutsideAngular(() => userErrorHandler.handleError(e));
}

@Injectable({providedIn: 'root'})
export class NgZoneChangeDetectionScheduler {
private readonly zone = inject(NgZone);
private readonly injector = inject(EnvironmentInjector);
private readonly applicationRef = inject(ApplicationRef);

// Lazy initialization to avoid circular DI since ApplicationRef initializes the scheduler.
// When Zoneless is the default, we can make the opt-in provider function have an
// ENVIRONMENT_INITIALIZER which initializes class instead of `ApplicationRef`.
private applicationRef?: ApplicationRef;
private _onMicrotaskEmptySubscription?: Subscription;

initialize(): void {
if (this._onMicrotaskEmptySubscription) {
return;
}

this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({
next: () => {
this.zone.run(() => {
this.applicationRef ??= this.injector.get(ApplicationRef);
this.applicationRef.tick();
});
}
Expand All @@ -1151,3 +1146,25 @@ export class NgZoneChangeDetectionScheduler {
this._onMicrotaskEmptySubscription?.unsubscribe();
}
}

export function provideNgZoneChangeDetection(ngZone: NgZone): StaticProvider[] {
return [
{provide: NgZone, useValue: ngZone},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory: () => {
const ngZoneChangeDetectionScheduler =
inject(NgZoneChangeDetectionScheduler, {optional: true});
if (NG_DEV_MODE && ngZoneChangeDetectionScheduler === null) {
throw new RuntimeError(
RuntimeErrorCode.MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP,
'No NgZoneChangeDetectionScheduler found in the Dependency Injection tree.');
}
return () => ngZoneChangeDetectionScheduler!.initialize();
},
},
{provide: INTERNAL_APPLICATION_ERROR_HANDLER, useFactory: ngZoneApplicationErrorHandlerFactory},
{provide: ZONE_IS_STABLE_OBSERVABLE, useFactory: isStableFactory},
];
}
2 changes: 1 addition & 1 deletion packages/core/src/core_private_export.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication} from './application_ref';
export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS, internalCreateApplication as ɵinternalCreateApplication, provideNgZoneChangeDetection as ɵprovideNgZoneChangeDetection} from './application_ref';
export {APP_ID_RANDOM_PROVIDER as ɵAPP_ID_RANDOM_PROVIDER} from './application_tokens';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {Console as ɵConsole} from './console';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/errors.ts
Expand Up @@ -52,7 +52,7 @@ export const enum RuntimeErrorCode {
// Bootstrap Errors
MULTIPLE_PLATFORMS = 400,
PLATFORM_NOT_FOUND = 401,
ERROR_HANDLER_NOT_FOUND = 402,
MISSING_REQUIRED_INJECTABLE_IN_BOOTSTRAP = 402,
BOOTSTRAP_COMPONENTS_NOT_FOUND = -403,
PLATFORM_ALREADY_DESTROYED = 404,
ASYNC_INITIALIZERS_STILL_RUNNING = 405,
Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/render3/ng_module_ref.ts
Expand Up @@ -8,7 +8,7 @@

import {createInjectorWithoutInjectorInstances} from '../di/create_injector';
import {Injector} from '../di/injector';
import {EnvironmentProviders, Provider} from '../di/interface/provider';
import {EnvironmentProviders, Provider, StaticProvider} from '../di/interface/provider';
import {EnvironmentInjector, getNullInjector, R3Injector} from '../di/r3_injector';
import {Type} from '../interface/type';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
Expand All @@ -32,7 +32,7 @@ import {maybeUnwrapFn} from './util/misc_utils';
*/
export function createNgModule<T>(
ngModule: Type<T>, parentInjector?: Injector): viewEngine_NgModuleRef<T> {
return new NgModuleRef<T>(ngModule, parentInjector ?? null);
return new NgModuleRef<T>(ngModule, parentInjector ?? null, []);
}

/**
Expand All @@ -59,7 +59,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
override readonly componentFactoryResolver: ComponentFactoryResolver =
new ComponentFactoryResolver(this);

constructor(ngModuleType: Type<T>, public _parent: Injector|null) {
constructor(
ngModuleType: Type<T>, public _parent: Injector|null, additionalProviders: StaticProvider[]) {
super();
const ngModuleDef = getNgModuleDef(ngModuleType);
ngDevMode &&
Expand All @@ -74,7 +75,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
{provide: viewEngine_NgModuleRef, useValue: this}, {
provide: viewEngine_ComponentFactoryResolver,
useValue: this.componentFactoryResolver
}
},
...additionalProviders
],
stringify(ngModuleType), new Set(['environment'])) as R3Injector;

Expand Down Expand Up @@ -108,10 +110,16 @@ export class NgModuleFactory<T> extends viewEngine_NgModuleFactory<T> {
}

override create(parentInjector: Injector|null): viewEngine_NgModuleRef<T> {
return new NgModuleRef(this.moduleType, parentInjector);
return new NgModuleRef(this.moduleType, parentInjector, []);
}
}

export function createNgModuleRefWithProviders<T>(
moduleType: Type<T>, parentInjector: Injector|null,
additionalProviders: StaticProvider[]): InternalNgModuleRef<T> {
return new NgModuleRef(moduleType, parentInjector, additionalProviders);
}

class EnvironmentNgModuleRefAdapter extends viewEngine_NgModuleRef<null> {
override readonly injector: EnvironmentInjector;
override readonly componentFactoryResolver: ComponentFactoryResolver =
Expand Down

0 comments on commit 4e098fa

Please sign in to comment.