Skip to content

Commit

Permalink
feat(core): Add ability to configure zone change detection to use zon…
Browse files Browse the repository at this point in the history
…eless scheduler (#55252)

This commit adds a configuration option to zone-based change detection
which allows applications to enable/disable the zoneless scheduler.
When the zoneless scheduler is enabled in zone-based applications,
updates that happen outside the Angular zone will still result in a
change detection being scheduled. Previously, Angular change detection
was solely based on the state of the Angular Zone.

PR Close #55252
  • Loading branch information
atscott committed Apr 12, 2024
1 parent c9abe77 commit fdd560e
Show file tree
Hide file tree
Showing 17 changed files with 42 additions and 67 deletions.
2 changes: 2 additions & 0 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function booleanAttribute(value: unknown): boolean;

// @public
export interface BootstrapOptions {
ignoreChangesOutsideZone?: boolean;
ngZone?: NgZone | 'zone.js' | 'noop';
ngZoneEventCoalescing?: boolean;
ngZoneRunCoalescing?: boolean;
Expand Down Expand Up @@ -1218,6 +1219,7 @@ export class NgZone {
// @public
export interface NgZoneOptions {
eventCoalescing?: boolean;
ignoreChangesOutsideZone?: boolean;
runCoalescing?: boolean;
}

Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/application/application_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@ export interface BootstrapOptions {
*
*/
ngZoneRunCoalescing?: boolean;

/**
* When false, change detection is scheduled when Angular receives
* a clear indication that templates need to be refreshed. This includes:
*
* - calling `ChangeDetectorRef.markForCheck`
* - calling `ComponentRef.setInput`
* - updating a signal that is read in a template
* - when bound host or template listeners are triggered
* - attaching a view that is marked dirty
* - removing a view
* - registering a render hook (templates are only refreshed if render hooks do one of the above)
*/
ignoreChangesOutsideZone?: boolean;
}

/** Maximum number of times ApplicationRef will refresh all attached views in a single tick. */
Expand Down
47 changes: 21 additions & 26 deletions packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,26 +59,9 @@ export class NgZoneChangeDetectionScheduler {
export const PROVIDED_NG_ZONE = new InjectionToken<boolean>(
(typeof ngDevMode === 'undefined' || ngDevMode) ? 'provideZoneChangeDetection token' : '');

/**
* Configures change detection scheduling when using ZoneJS.
*/
export enum SchedulingMode {
/**
* Change detection will run when the `NgZone.onMicrotaskEmpty` observable emits.
* Change detection will also be scheduled to run whenever Angular is notified
* of a change. This includes calling `ChangeDetectorRef.markForCheck`,
* setting a `signal` value, and attaching a view.
*/
Hybrid,
/**
* Change detection will only run when the `NgZone.onMicrotaskEmpty` observable emits.
*/
NgZoneOnly,
}

export function internalProvideZoneChangeDetection(
{ngZoneFactory, schedulingMode}:
{ngZoneFactory: () => NgZone, schedulingMode?: SchedulingMode}): StaticProvider[] {
{ngZoneFactory, ignoreChangesOutsideZone}:
{ngZoneFactory: () => NgZone, ignoreChangesOutsideZone?: boolean}): StaticProvider[] {
return [
{provide: NgZone, useFactory: ngZoneFactory},
{
Expand Down Expand Up @@ -109,11 +92,9 @@ export function internalProvideZoneChangeDetection(
},
{provide: INTERNAL_APPLICATION_ERROR_HANDLER, useFactory: ngZoneApplicationErrorHandlerFactory},
// Always disable scheduler whenever explicitly disabled, even if Hybrid was specified elsewhere
schedulingMode === SchedulingMode.NgZoneOnly ?
{provide: ZONELESS_SCHEDULER_DISABLED, useValue: true} :
[],
// Only provide scheduler when explicitly enabled
schedulingMode === SchedulingMode.Hybrid ?
ignoreChangesOutsideZone === true ? {provide: ZONELESS_SCHEDULER_DISABLED, useValue: true} : [],
// Only provide scheduler when explicitly not disabled
ignoreChangesOutsideZone === false ?
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl} :
[],
];
Expand Down Expand Up @@ -146,7 +127,7 @@ export function ngZoneApplicationErrorHandlerFactory() {
* @see {@link NgZoneOptions}
*/
export function provideZoneChangeDetection(options?: NgZoneOptions): EnvironmentProviders {
const schedulingMode = (options as any)?.schedulingMode;
const ignoreChangesOutsideZone = options?.ignoreChangesOutsideZone;
const zoneProviders = internalProvideZoneChangeDetection({
ngZoneFactory: () => {
const ngZoneOptions = getNgZoneOptions(options);
Expand All @@ -155,7 +136,7 @@ export function provideZoneChangeDetection(options?: NgZoneOptions): Environment
}
return new NgZone(ngZoneOptions);
},
schedulingMode
ignoreChangesOutsideZone
});
return makeEnvironmentProviders([
(typeof ngDevMode === 'undefined' || ngDevMode) ? {provide: PROVIDED_NG_ZONE, useValue: true} :
Expand Down Expand Up @@ -214,6 +195,20 @@ export interface NgZoneOptions {
*
*/
runCoalescing?: boolean;

/**
* When false, change detection is scheduled when Angular receives
* a clear indication that templates need to be refreshed. This includes:
*
* - calling `ChangeDetectorRef.markForCheck`
* - calling `ComponentRef.setInput`
* - updating a signal that is read in a template
* - when bound host or template listeners are triggered
* - attaching a view that is marked dirty
* - removing a view
* - registering a render hook (templates are only refreshed if render hooks do one of the above)
*/
ignoreChangesOutsideZone?: boolean;
}

// Transforms a set of `BootstrapOptions` (supported by the NgModule-based bootstrap APIs) ->
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/core_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export {detectChangesInViewIfRequired as ɵdetectChangesInViewIfRequired, whenSt
export {IMAGE_CONFIG as ɵIMAGE_CONFIG, IMAGE_CONFIG_DEFAULTS as ɵIMAGE_CONFIG_DEFAULTS, ImageConfig as ɵImageConfig} from './application/application_tokens';
export {internalCreateApplication as ɵinternalCreateApplication} from './application/create_application';
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
export {SchedulingMode as ɵSchedulingMode} from './change_detection/scheduling/ng_zone_scheduling';
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler, ZONELESS_ENABLED as ɵZONELESS_ENABLED} from './change_detection/scheduling/zoneless_scheduling';
export {provideZonelessChangeDetection as ɵprovideZonelessChangeDetection} from './change_detection/scheduling/zoneless_scheduling_impl';
export {Console as ɵConsole} from './console';
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/platform/platform_ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import {ApplicationInitStatus} from '../application/application_init';
import {compileNgModuleFactory} from '../application/application_ngmodule_factory_compiler';
import {_callAndReportToErrorHandler, ApplicationRef, BootstrapOptions, optionsReducer, remove} from '../application/application_ref';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE, SchedulingMode} from '../change_detection/scheduling/ng_zone_scheduling';
import {getNgZoneOptions, internalProvideZoneChangeDetection, PROVIDED_NG_ZONE} from '../change_detection/scheduling/ng_zone_scheduling';
import {Injectable, InjectionToken, Injector} from '../di';
import {ErrorHandler} from '../error_handler';
import {RuntimeError, RuntimeErrorCode} from '../errors';
Expand Down Expand Up @@ -72,11 +72,12 @@ export class PlatformRef {
// 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 schedulingMode = (options as any)?.schedulingMode;
const ignoreChangesOutsideZone = options?.ignoreChangesOutsideZone;
const moduleRef = createNgModuleRefWithProviders(
moduleFactory.moduleType,
this.injector,
internalProvideZoneChangeDetection({ngZoneFactory: () => ngZone, schedulingMode}),
internalProvideZoneChangeDetection(
{ngZoneFactory: () => ngZone, ignoreChangesOutsideZone}),
);

if ((typeof ngDevMode === 'undefined' || ngDevMode) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,9 +383,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "SimpleChange"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "SecurityContext"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -413,9 +413,6 @@
{
"name": "Sanitizer"
},
{
"name": "SchedulingMode"
},
{
"name": "ShadowDomRenderer"
},
Expand Down
5 changes: 1 addition & 4 deletions packages/core/test/change_detection_scheduler_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,9 +543,6 @@ describe('Angular with zoneless enabled', () => {
});

describe('Angular with scheduler and ZoneJS', () => {
// TODO(atscott): Update once option is public
const hybridModeSchedulingOptions = {schedulingMode: 0} as any;

beforeEach(() => {
TestBed.configureTestingModule(
{providers: [{provide: ComponentFixtureAutoDetect, useValue: true}]});
Expand All @@ -571,7 +568,7 @@ describe('Angular with scheduler and ZoneJS', () => {

it('updating signal outside of zone still schedules update when in hybrid mode', async () => {
TestBed.configureTestingModule(
{providers: [provideZoneChangeDetection(hybridModeSchedulingOptions)]});
{providers: [provideZoneChangeDetection({ignoreChangesOutsideZone: false})]});
@Component({template: '{{thing()}}', standalone: true})
class App {
thing = signal('initial');
Expand Down

0 comments on commit fdd560e

Please sign in to comment.