mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(core): Allow hybrid CD scheduling to support multiple "Angular zones" (#57267)
This commit updates the inside/outside NgZone detection of the hybrid CD scheduling to track the actual instance of the NgZone being used rather than the name "Angular" (how `isInsideAngularZone` works). This allows the scheduling to work correctly when there are multiple versions of Angular running on the page. fixes #57261 PR Close #57267
This commit is contained in:
committed by
Andrew Kushnir
parent
3439cc2049
commit
769b6e1973
@@ -20,7 +20,7 @@ import {
|
||||
scheduleCallbackWithRafRace,
|
||||
} from '../../util/callback_scheduler';
|
||||
import {performanceMarkFeature} from '../../util/performance';
|
||||
import {NgZone, NoopNgZone} from '../../zone/ng_zone';
|
||||
import {NgZone, NgZonePrivate, NoopNgZone, angularZoneInstanceIdProperty} from '../../zone/ng_zone';
|
||||
|
||||
import {
|
||||
ChangeDetectionScheduler,
|
||||
@@ -64,6 +64,9 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.run;
|
||||
private readonly schedulerTickApplyArgs = [{data: {'__scheduler_tick__': true}}];
|
||||
private readonly subscriptions = new Subscription();
|
||||
private readonly angularZoneId = this.zoneIsDefined
|
||||
? (this.ngZone as NgZonePrivate)._inner?.get(angularZoneInstanceIdProperty)
|
||||
: null;
|
||||
|
||||
private cancelScheduledCallback: null | (() => void) = null;
|
||||
private shouldRefreshViews = false;
|
||||
@@ -176,7 +179,11 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
}
|
||||
// If we're inside the zone don't bother with scheduler. Zone will stabilize
|
||||
// eventually and run change detection.
|
||||
if (!this.zonelessEnabled && this.zoneIsDefined && NgZone.isInAngularZone()) {
|
||||
if (
|
||||
!this.zonelessEnabled &&
|
||||
this.zoneIsDefined &&
|
||||
Zone.current.get(angularZoneInstanceIdProperty + this.angularZoneId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ import {AsyncStackTaggingZoneSpec} from './async-stack-tagging';
|
||||
// ERROR - [JSC_UNDEFINED_VARIABLE] variable Zone is undeclared
|
||||
declare const Zone: any;
|
||||
|
||||
const isAngularZoneProperty = 'isAngularZone';
|
||||
export const angularZoneInstanceIdProperty = isAngularZoneProperty + '_ID';
|
||||
|
||||
let ngZoneInstanceId = 0;
|
||||
|
||||
/**
|
||||
* An injectable service for executing work inside or outside of the Angular zone.
|
||||
*
|
||||
@@ -173,7 +178,7 @@ export class NgZone {
|
||||
*/
|
||||
static isInAngularZone(): boolean {
|
||||
// Zone needs to be checked, because this method might be called even when NoopNgZone is used.
|
||||
return typeof Zone !== 'undefined' && Zone.current.get('isAngularZone') === true;
|
||||
return typeof Zone !== 'undefined' && Zone.current.get(isAngularZoneProperty) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,7 +271,7 @@ export class NgZone {
|
||||
|
||||
const EMPTY_PAYLOAD = {};
|
||||
|
||||
interface NgZonePrivate extends NgZone {
|
||||
export interface NgZonePrivate extends NgZone {
|
||||
_outer: Zone;
|
||||
_inner: Zone;
|
||||
_nesting: number;
|
||||
@@ -394,9 +399,14 @@ function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
|
||||
const delayChangeDetectionForEventsDelegate = () => {
|
||||
delayChangeDetectionForEvents(zone);
|
||||
};
|
||||
const instanceId = ngZoneInstanceId++;
|
||||
zone._inner = zone._inner.fork({
|
||||
name: 'angular',
|
||||
properties: <any>{'isAngularZone': true},
|
||||
properties: <any>{
|
||||
[isAngularZoneProperty]: true,
|
||||
[angularZoneInstanceIdProperty]: instanceId,
|
||||
[angularZoneInstanceIdProperty + instanceId]: true,
|
||||
},
|
||||
onInvokeTask: (
|
||||
delegate: ZoneDelegate,
|
||||
current: Zone,
|
||||
|
||||
@@ -653,6 +653,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "animate"
|
||||
},
|
||||
@@ -1118,6 +1121,9 @@
|
||||
{
|
||||
"name": "invokeQuery"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1259,6 +1265,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "nonNull"
|
||||
},
|
||||
|
||||
@@ -710,6 +710,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "animate"
|
||||
},
|
||||
@@ -1187,6 +1190,9 @@
|
||||
{
|
||||
"name": "invokeQuery"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1325,6 +1331,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
|
||||
@@ -536,6 +536,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -941,6 +944,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1067,6 +1073,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
|
||||
@@ -602,6 +602,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -2153,6 +2156,9 @@
|
||||
{
|
||||
"name": "invokeTriggerCleanupFns"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -2291,6 +2297,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "nonNull"
|
||||
},
|
||||
|
||||
@@ -758,6 +758,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -1364,6 +1367,9 @@
|
||||
{
|
||||
"name": "isAbstractControlOptions"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1589,6 +1595,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
|
||||
@@ -740,6 +740,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -1322,6 +1325,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1556,6 +1562,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
|
||||
@@ -398,6 +398,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "applyNodes"
|
||||
},
|
||||
@@ -749,6 +752,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -839,6 +845,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noop"
|
||||
},
|
||||
|
||||
@@ -587,6 +587,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -1019,6 +1022,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1175,6 +1181,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "nonNull"
|
||||
},
|
||||
|
||||
@@ -878,6 +878,9 @@
|
||||
{
|
||||
"name": "allowSanitizationBypassAndThrow"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -1553,6 +1556,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1784,6 +1790,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noMatch"
|
||||
},
|
||||
|
||||
@@ -482,6 +482,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -839,6 +842,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -944,6 +950,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "nonNull"
|
||||
},
|
||||
|
||||
@@ -629,6 +629,9 @@
|
||||
{
|
||||
"name": "allocLFrame"
|
||||
},
|
||||
{
|
||||
"name": "angularZoneInstanceIdProperty"
|
||||
},
|
||||
{
|
||||
"name": "appendChild"
|
||||
},
|
||||
@@ -1124,6 +1127,9 @@
|
||||
{
|
||||
"name": "invokeHostBindingsInCreationMode"
|
||||
},
|
||||
{
|
||||
"name": "isAngularZoneProperty"
|
||||
},
|
||||
{
|
||||
"name": "isApplicationBootstrapConfig"
|
||||
},
|
||||
@@ -1283,6 +1289,9 @@
|
||||
{
|
||||
"name": "ngOnChangesSetInput"
|
||||
},
|
||||
{
|
||||
"name": "ngZoneInstanceId"
|
||||
},
|
||||
{
|
||||
"name": "noSideEffects"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
Component,
|
||||
createComponent,
|
||||
destroyPlatform,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EnvironmentInjector,
|
||||
ErrorHandler,
|
||||
@@ -43,6 +42,7 @@ import {filter, take, tap} from 'rxjs/operators';
|
||||
|
||||
import {RuntimeError, RuntimeErrorCode} from '../src/errors';
|
||||
import {scheduleCallbackWithRafRace} from '../src/util/callback_scheduler';
|
||||
import {ChangeDetectionSchedulerImpl} from '../src/change_detection/scheduling/zoneless_scheduling_impl';
|
||||
import {global} from '../src/util/global';
|
||||
|
||||
function isStable(injector = TestBed.inject(EnvironmentInjector)): boolean {
|
||||
@@ -737,6 +737,59 @@ describe('Angular with scheduler and ZoneJS', () => {
|
||||
expect(fixture.nativeElement.innerText).toContain('new');
|
||||
});
|
||||
|
||||
it('updating signal in another "Angular" zone schedules update when in hybrid mode', async () => {
|
||||
@Component({template: '{{thing()}}', standalone: true})
|
||||
class App {
|
||||
thing = signal('initial');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
expect(fixture.nativeElement.innerText).toContain('initial');
|
||||
const differentAngularZone: NgZone = Zone.root.run(() => new NgZone({}));
|
||||
differentAngularZone.run(() => {
|
||||
fixture.componentInstance.thing.set('new');
|
||||
});
|
||||
|
||||
expect(fixture.isStable()).toBe(false);
|
||||
await fixture.whenStable();
|
||||
expect(fixture.nativeElement.innerText).toContain('new');
|
||||
});
|
||||
|
||||
it('updating signal in a child zone of Angular does not schedule extra CD', async () => {
|
||||
@Component({template: '{{thing()}}', standalone: true})
|
||||
class App {
|
||||
thing = signal('initial');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
expect(fixture.nativeElement.innerText).toContain('initial');
|
||||
const childZone = TestBed.inject(NgZone).run(() => Zone.current.fork({name: 'child'}));
|
||||
|
||||
childZone.run(() => {
|
||||
fixture.componentInstance.thing.set('new');
|
||||
expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('updating signal in a child Angular zone of Angular does not schedule extra CD', async () => {
|
||||
@Component({template: '{{thing()}}', standalone: true})
|
||||
class App {
|
||||
thing = signal('initial');
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(App);
|
||||
await fixture.whenStable();
|
||||
expect(fixture.nativeElement.innerText).toContain('initial');
|
||||
const childAngularZone = TestBed.inject(NgZone).run(() => new NgZone({}));
|
||||
|
||||
childAngularZone.run(() => {
|
||||
fixture.componentInstance.thing.set('new');
|
||||
expect(TestBed.inject(ChangeDetectionSchedulerImpl)['cancelScheduledCallback']).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not run change detection twice if notified during AppRef.tick', async () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
||||
Reference in New Issue
Block a user