Skip to content

Commit e711f99

Browse files
atscottmmalerba
authored andcommitted
feat(core): move provideExperimentalCheckNoChangesForDebug to provideCheckNoChangesConfig (angular#60906)
This commit makes several changes changes to the `provideExperimentalCheckNoChangesForDebug` API: * Rename it * Promote to dev preview * Apply the exhaustive behavior to _all_ checkNoChanges runs * Remove `useNgZoneOnStable` option. This wasn't found to be generally more useful than `interval` fixes angular#45612 BREAKING CHANGE: `provideExperimentalCheckNoChangesForDebug` has several breaking changes: * It is renamed to `provideCheckNoChangesConfig` * The behavior applies to _all_ checkNoChanges runs * The `useNgZoneOnStable` option is removed. This wasn't found to be generally more useful than `interval` PR Close angular#60906
1 parent 148f467 commit e711f99

File tree

12 files changed

+155
-293
lines changed

12 files changed

+155
-293
lines changed

adev/src/content/guide/zoneless.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ it is acceptable to use `fixture.changeDetectorRef.markForCheck()`.
161161
### Debug-mode check to ensure updates are detected
162162

163163
Angular also provides an additional tool to help verify that an application is making
164-
updates to state in a zoneless-compatible way. `provideExperimentalCheckNoChangesForDebug`
164+
updates to state in a zoneless-compatible way. `provideCheckNoChangesConfig({exhaustive: true, interval: <milliseconds>})`
165165
can be used to periodically check to ensure that no bindings have been updated
166166
without a notification. Angular throws `ExpressionChangedAfterItHasBeenCheckedError`
167167
if there is an updated binding that would not have refreshed by the zoneless change

goldens/public-api/core/index.api.md

+9-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
```ts
66

7-
import * as _angular_core from '@angular/core';
87
import { Observable } from 'rxjs';
98
import { Subject } from 'rxjs';
109
import { Subscription } from 'rxjs';
@@ -1450,14 +1449,18 @@ export function provideAppInitializer(initializerFn: () => Observable<unknown> |
14501449
export function provideBrowserGlobalErrorListeners(): EnvironmentProviders;
14511450

14521451
// @public
1453-
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;
1452+
export function provideCheckNoChangesConfig(options: {
1453+
exhaustive: false;
1454+
}): EnvironmentProviders;
14541455

14551456
// @public
1456-
export function provideExperimentalCheckNoChangesForDebug(options: {
1457+
export function provideCheckNoChangesConfig(options: {
14571458
interval?: number;
1458-
useNgZoneOnStable?: boolean;
1459-
exhaustive?: boolean;
1460-
}): _angular_core.EnvironmentProviders;
1459+
exhaustive: true;
1460+
}): EnvironmentProviders;
1461+
1462+
// @public
1463+
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;
14611464

14621465
// @public
14631466
export function provideNgReflectAttributes(): EnvironmentProviders;

packages/core/src/change_detection/change_detection.ts

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
KeyValueDifferFactory,
3737
KeyValueDiffers,
3838
} from './differs/keyvalue_differs';
39+
3940
export {PipeTransform} from './pipe_transform';
4041

4142
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {EnvironmentProviders, makeEnvironmentProviders} from '../di';
10+
import {UseExhaustiveCheckNoChanges} from './use_exhaustive_check_no_changes';
11+
import {exhaustiveCheckNoChangesInterval} from './scheduling/exhaustive_check_no_changes';
12+
13+
/**
14+
* Used to disable exhaustive checks when verifying no expressions changed after they were checked.
15+
*
16+
* This means that `OnPush` components that are not marked for check will not be checked.
17+
* This behavior is the current default behavior in Angular. When running change detection
18+
* on a view tree, views marked for check are refreshed and the flag to check it is removed.
19+
* When Angular checks views a second time to ensure nothing has changed, `OnPush` components
20+
* will no longer be marked and not be checked.
21+
*
22+
* @developerPreview
23+
* @publicApi
24+
*/
25+
export function provideCheckNoChangesConfig(options: {exhaustive: false}): EnvironmentProviders;
26+
/**
27+
* - `interval` will periodically run `checkNoChanges` on application views. This can be useful
28+
* in zoneless applications to periodically ensure no changes have been made without notifying
29+
* Angular that templates need to be refreshed.
30+
* - The exhaustive option will treat all application views as if they were `ChangeDetectionStrategy.Default` when verifying
31+
* no expressions have changed. All views attached to `ApplicationRef` and all the descendants of
32+
* those views will be checked for changes (excluding those subtrees which are detached via `ChangeDetectorRef.detach()`).
33+
* This is useful because the check that runs after regular change detection does not work for components using `ChangeDetectionStrategy.OnPush`.
34+
* This check is will surface any existing errors hidden by `OnPush` components.
35+
*
36+
* @developerPreview
37+
* @publicApi
38+
*/
39+
export function provideCheckNoChangesConfig(options: {
40+
interval?: number;
41+
exhaustive: true;
42+
}): EnvironmentProviders;
43+
export function provideCheckNoChangesConfig(options: {
44+
interval?: number;
45+
exhaustive: boolean;
46+
}): EnvironmentProviders {
47+
return makeEnvironmentProviders(
48+
typeof ngDevMode === 'undefined' || ngDevMode
49+
? [
50+
{
51+
provide: UseExhaustiveCheckNoChanges,
52+
useValue: options.exhaustive,
53+
},
54+
options?.interval !== undefined ? exhaustiveCheckNoChangesInterval(options.interval) : [],
55+
]
56+
: [],
57+
);
58+
}

packages/core/src/change_detection/scheduling/exhaustive_check_no_changes.ts

+28-148
Original file line numberDiff line numberDiff line change
@@ -9,162 +9,42 @@
99
import {ApplicationRef} from '../../application/application_ref';
1010
import {ChangeDetectionSchedulerImpl} from './zoneless_scheduling_impl';
1111
import {inject} from '../../di/injector_compatibility';
12-
import {makeEnvironmentProviders} from '../../di/provider_collection';
12+
import {provideEnvironmentInitializer} from '../../di/provider_collection';
1313
import {NgZone} from '../../zone/ng_zone';
1414

15-
import {EnvironmentInjector} from '../../di/r3_injector';
16-
import {ENVIRONMENT_INITIALIZER} from '../../di/initializer_token';
17-
import {CheckNoChangesMode} from '../../render3/state';
1815
import {ErrorHandler} from '../../error_handler';
1916
import {checkNoChangesInternal} from '../../render3/instructions/change_detection';
20-
import {ZONELESS_ENABLED} from './zoneless_scheduling';
2117

22-
/**
23-
* Used to periodically verify no expressions have changed after they were checked.
24-
*
25-
* @param options Used to configure when the check will execute.
26-
* - `interval` will periodically run exhaustive `checkNoChanges` on application views
27-
* - `useNgZoneOnStable` will use ZoneJS to determine when change detection might have run
28-
* in an application using ZoneJS to drive change detection. When the `NgZone.onStable` would
29-
* have emitted, all views attached to the `ApplicationRef` are checked for changes.
30-
* - 'exhaustive' means that all views attached to `ApplicationRef` and all the descendants of those views will be
31-
* checked for changes (excluding those subtrees which are detached via `ChangeDetectorRef.detach()`).
32-
* This is useful because the check that runs after regular change detection does not work for components using `ChangeDetectionStrategy.OnPush`.
33-
* This check is will surface any existing errors hidden by `OnPush` components. By default, this check is exhaustive
34-
* and will always check all views, regardless of their "dirty" state and `ChangeDetectionStrategy`.
35-
*
36-
* When the `useNgZoneOnStable` option is `true`, this function will provide its own `NgZone` implementation and needs
37-
* to come after any other `NgZone` provider, including `provideZoneChangeDetection()` and `provideZonelessChangeDetection()`.
38-
*
39-
* @experimental
40-
* @publicApi
41-
*/
42-
export function provideExperimentalCheckNoChangesForDebug(options: {
43-
interval?: number;
44-
useNgZoneOnStable?: boolean;
45-
exhaustive?: boolean;
46-
}) {
47-
if (typeof ngDevMode === 'undefined' || ngDevMode) {
48-
if (options.interval === undefined && !options.useNgZoneOnStable) {
49-
throw new Error('Must provide one of `useNgZoneOnStable` or `interval`');
50-
}
51-
const checkNoChangesMode =
52-
options?.exhaustive === false
53-
? CheckNoChangesMode.OnlyDirtyViews
54-
: CheckNoChangesMode.Exhaustive;
55-
return makeEnvironmentProviders([
56-
options?.useNgZoneOnStable
57-
? {provide: NgZone, useFactory: () => new DebugNgZoneForCheckNoChanges(checkNoChangesMode)}
58-
: [],
59-
options?.interval !== undefined
60-
? exhaustiveCheckNoChangesInterval(options.interval, checkNoChangesMode)
61-
: [],
62-
{
63-
provide: ENVIRONMENT_INITIALIZER,
64-
multi: true,
65-
useValue: () => {
66-
if (
67-
options?.useNgZoneOnStable &&
68-
!(inject(NgZone) instanceof DebugNgZoneForCheckNoChanges)
69-
) {
70-
throw new Error(
71-
'`provideExperimentalCheckNoChangesForDebug` with `useNgZoneOnStable` must be after any other provider for `NgZone`.',
72-
);
18+
export function exhaustiveCheckNoChangesInterval(interval: number) {
19+
return provideEnvironmentInitializer(() => {
20+
const applicationRef = inject(ApplicationRef);
21+
const errorHandler = inject(ErrorHandler);
22+
const scheduler = inject(ChangeDetectionSchedulerImpl);
23+
const ngZone = inject(NgZone);
24+
25+
function scheduleCheckNoChanges() {
26+
ngZone.runOutsideAngular(() => {
27+
setTimeout(() => {
28+
if (applicationRef.destroyed) {
29+
return;
30+
}
31+
if (scheduler.pendingRenderTaskId || scheduler.runningTick) {
32+
scheduleCheckNoChanges();
33+
return;
7334
}
74-
},
75-
},
76-
]);
77-
} else {
78-
return makeEnvironmentProviders([]);
79-
}
80-
}
81-
82-
export class DebugNgZoneForCheckNoChanges extends NgZone {
83-
private applicationRef?: ApplicationRef;
84-
private scheduler?: ChangeDetectionSchedulerImpl;
85-
private errorHandler?: ErrorHandler;
86-
private readonly injector = inject(EnvironmentInjector);
8735

88-
constructor(private readonly checkNoChangesMode: CheckNoChangesMode) {
89-
const zonelessEnabled = inject(ZONELESS_ENABLED);
90-
// Use coalescing to ensure we aren't ever running this check synchronously
91-
super({
92-
shouldCoalesceEventChangeDetection: true,
93-
shouldCoalesceRunChangeDetection: zonelessEnabled,
94-
});
36+
for (const view of applicationRef.allViews) {
37+
try {
38+
checkNoChangesInternal(view._lView, true /** exhaustive */);
39+
} catch (e) {
40+
errorHandler.handleError(e);
41+
}
42+
}
9543

96-
if (zonelessEnabled) {
97-
// prevent emits to ensure code doesn't rely on these
98-
this.onMicrotaskEmpty.emit = () => {};
99-
this.onStable.emit = () => {
100-
this.scheduler ||= this.injector.get(ChangeDetectionSchedulerImpl);
101-
if (this.scheduler.pendingRenderTaskId || this.scheduler.runningTick) {
102-
return;
103-
}
104-
this.checkApplicationViews();
105-
};
106-
this.onUnstable.emit = () => {};
107-
} else {
108-
this.runOutsideAngular(() => {
109-
this.onStable.subscribe(() => {
110-
this.checkApplicationViews();
111-
});
44+
scheduleCheckNoChanges();
45+
}, interval);
11246
});
11347
}
114-
}
115-
116-
private checkApplicationViews() {
117-
this.applicationRef ||= this.injector.get(ApplicationRef);
118-
for (const view of this.applicationRef.allViews) {
119-
try {
120-
checkNoChangesInternal(view._lView, this.checkNoChangesMode);
121-
} catch (e) {
122-
this.errorHandler ||= this.injector.get(ErrorHandler);
123-
this.errorHandler.handleError(e);
124-
}
125-
}
126-
}
127-
}
128-
129-
function exhaustiveCheckNoChangesInterval(
130-
interval: number,
131-
checkNoChangesMode: CheckNoChangesMode,
132-
) {
133-
return {
134-
provide: ENVIRONMENT_INITIALIZER,
135-
multi: true,
136-
useFactory: () => {
137-
const applicationRef = inject(ApplicationRef);
138-
const errorHandler = inject(ErrorHandler);
139-
const scheduler = inject(ChangeDetectionSchedulerImpl);
140-
const ngZone = inject(NgZone);
141-
142-
return () => {
143-
function scheduleCheckNoChanges() {
144-
ngZone.runOutsideAngular(() => {
145-
setTimeout(() => {
146-
if (applicationRef.destroyed) {
147-
return;
148-
}
149-
if (scheduler.pendingRenderTaskId || scheduler.runningTick) {
150-
scheduleCheckNoChanges();
151-
return;
152-
}
153-
154-
for (const view of applicationRef.allViews) {
155-
try {
156-
checkNoChangesInternal(view._lView, checkNoChangesMode);
157-
} catch (e) {
158-
errorHandler.handleError(e);
159-
}
160-
}
161-
162-
scheduleCheckNoChanges();
163-
}, interval);
164-
});
165-
}
166-
scheduleCheckNoChanges();
167-
};
168-
},
169-
};
48+
scheduleCheckNoChanges();
49+
});
17050
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {InjectionToken} from '../di';
10+
11+
export const USE_EXHAUSTIVE_CHECK_NO_CHANGES_DEFAULT = false;
12+
13+
export const UseExhaustiveCheckNoChanges = new InjectionToken<boolean>(
14+
ngDevMode ? 'exhaustive checkNoChanges' : '',
15+
);

packages/core/src/core.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export {
4949
provideZonelessChangeDetection as provideExperimentalZonelessChangeDetection,
5050
} from './change_detection/scheduling/zoneless_scheduling_impl';
5151
export {PendingTasks} from './pending_tasks';
52-
export {provideExperimentalCheckNoChangesForDebug} from './change_detection/scheduling/exhaustive_check_no_changes';
52+
export {provideCheckNoChangesConfig} from './change_detection/provide_check_no_changes_config';
5353
export {enableProdMode, isDevMode} from './util/is_dev_mode';
5454
export {
5555
APP_ID,

packages/core/src/render3/instructions/change_detection.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ function detectChangesInViewWhileDirty(lView: LView, mode: ChangeDetectionMode)
137137
}
138138
}
139139

140-
export function checkNoChangesInternal(lView: LView, mode: CheckNoChangesMode) {
141-
setIsInCheckNoChangesMode(mode);
140+
export function checkNoChangesInternal(lView: LView, exhaustive: boolean) {
141+
setIsInCheckNoChangesMode(
142+
exhaustive ? CheckNoChangesMode.Exhaustive : CheckNoChangesMode.OnlyDirtyViews,
143+
);
142144
try {
143145
detectChangesInternal(lView);
144146
} finally {

packages/core/src/render3/view_ref.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {
10+
USE_EXHAUSTIVE_CHECK_NO_CHANGES_DEFAULT,
11+
UseExhaustiveCheckNoChanges,
12+
} from '../change_detection/use_exhaustive_check_no_changes';
913
import type {ChangeDetectorRef} from '../change_detection/change_detector_ref';
1014
import {NotificationSource} from '../change_detection/scheduling/zoneless_scheduling';
1115
import type {ApplicationRef} from '../core';
@@ -23,6 +27,7 @@ import {
2327
CONTEXT,
2428
DECLARATION_LCONTAINER,
2529
FLAGS,
30+
INJECTOR,
2631
LView,
2732
LViewFlags,
2833
PARENT,
@@ -46,6 +51,7 @@ interface ChangeDetectorRefInterface extends ChangeDetectorRef {}
4651
export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterface {
4752
private _appRef: ApplicationRef | null = null;
4853
private _attachedToViewContainer = false;
54+
private readonly exhaustive: boolean = USE_EXHAUSTIVE_CHECK_NO_CHANGES_DEFAULT;
4955

5056
get rootNodes(): any[] {
5157
const lView = this._lView;
@@ -74,7 +80,14 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
7480
* This may be different from `_lView` if the `_cdRefInjectingView` is an embedded view.
7581
*/
7682
private _cdRefInjectingView?: LView,
77-
) {}
83+
) {
84+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
85+
this.exhaustive = this._lView[INJECTOR].get(
86+
UseExhaustiveCheckNoChanges,
87+
USE_EXHAUSTIVE_CHECK_NO_CHANGES_DEFAULT,
88+
);
89+
}
90+
}
7891

7992
get context(): T {
8093
return this._lView[CONTEXT] as unknown as T;
@@ -320,9 +333,8 @@ export class ViewRef<T> implements EmbeddedViewRef<T>, ChangeDetectorRefInterfac
320333
* introduce other changes.
321334
*/
322335
checkNoChanges(): void {
323-
if (ngDevMode) {
324-
checkNoChangesInternal(this._lView, CheckNoChangesMode.OnlyDirtyViews);
325-
}
336+
if (!ngDevMode) return;
337+
checkNoChangesInternal(this._lView, this.exhaustive);
326338
}
327339

328340
attachToViewContainerRef() {

0 commit comments

Comments
 (0)