mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): Move change detection scheduler implementation to core (#53579)
This commit moves the implementation of the change detection scheduler used for testing to angular/core along with a (private export) provider function. Note: Naming of the provider function is absolutely not final (and not public API). I would prefer one that did not mention "zones" but the easiest thing for now is to have a "Zone" and "Zoneless" naming scheme. PR Close #53579
This commit is contained in:
parent
60dfcc209e
commit
5978b3d132
4 changed files with 107 additions and 65 deletions
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef} from '../../application/application_ref';
|
||||
import {EnvironmentProviders, inject, Injectable, makeEnvironmentProviders} from '../../di';
|
||||
import {PendingTasks} from '../../pending_tasks';
|
||||
import {NgZone, NoopNgZone} from '../../zone/ng_zone';
|
||||
|
||||
import {ChangeDetectionScheduler} from './zoneless_scheduling';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private appRef = inject(ApplicationRef);
|
||||
private taskService = inject(PendingTasks);
|
||||
private pendingRenderTaskId: number|null = null;
|
||||
|
||||
notify(): void {
|
||||
if (this.pendingRenderTaskId !== null) return;
|
||||
|
||||
this.pendingRenderTaskId = this.taskService.add();
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (!this.appRef.destroyed) {
|
||||
this.appRef.tick();
|
||||
}
|
||||
} finally {
|
||||
// If this is the last task, the service will synchronously emit a stable notification. If
|
||||
// there is a subscriber that then acts in a way that tries to notify the scheduler again,
|
||||
// we need to be able to respond to schedule a new change detection. Therefore, we should
|
||||
// clear the task ID before removing it from the pending tasks (or the tasks service should
|
||||
// not synchronously emit stable, similar to how Zone stableness only happens if it's still
|
||||
// stable after a microtask).
|
||||
const taskId = this.pendingRenderTaskId!;
|
||||
this.pendingRenderTaskId = null;
|
||||
this.taskService.remove(taskId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function provideZonelessChangeDetection(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
|
||||
{provide: NgZone, useClass: NoopNgZone},
|
||||
]);
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export {internalCreateApplication as ɵinternalCreateApplication} from './applic
|
|||
export {defaultIterableDiffers as ɵdefaultIterableDiffers, defaultKeyValueDiffers as ɵdefaultKeyValueDiffers} from './change_detection/change_detection';
|
||||
export {getEnsureDirtyViewsAreAlwaysReachable as ɵgetEnsureDirtyViewsAreAlwaysReachable, setEnsureDirtyViewsAreAlwaysReachable as ɵsetEnsureDirtyViewsAreAlwaysReachable} from './change_detection/flags';
|
||||
export {ChangeDetectionScheduler as ɵChangeDetectionScheduler} from './change_detection/scheduling/zoneless_scheduling';
|
||||
export {provideZonelessChangeDetection as ɵprovideZonelessChangeDetection} from './change_detection/scheduling/zoneless_scheduling_impl';
|
||||
export {Console as ɵConsole} from './console';
|
||||
export {DeferBlockDetails as ɵDeferBlockDetails, getDeferBlocks as ɵgetDeferBlocks} from './defer/discovery';
|
||||
export {renderDeferBlockState as ɵrenderDeferBlockState, triggerResourceLoading as ɵtriggerResourceLoading} from './defer/instructions';
|
||||
|
|
|
|||
|
|
@ -1922,6 +1922,9 @@
|
|||
{
|
||||
"name": "init_zoneless_scheduling"
|
||||
},
|
||||
{
|
||||
"name": "init_zoneless_scheduling_impl"
|
||||
},
|
||||
{
|
||||
"name": "initializeDirectives"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,42 +7,17 @@
|
|||
*/
|
||||
|
||||
import {AsyncPipe} from '@angular/common';
|
||||
import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id';
|
||||
import {ApplicationRef, ChangeDetectorRef,ErrorHandler, Component, ComponentRef, createComponent, DebugElement, ElementRef, EnvironmentInjector, getDebugNode, inject, Injectable, Input, NgZone, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵNoopNgZone, ɵPendingTasks as PendingTasks} from '@angular/core';
|
||||
import {ApplicationRef, ChangeDetectorRef, Component, ComponentRef, createComponent, DebugElement, ElementRef, EnvironmentInjector, ErrorHandler, getDebugNode, inject, Injectable, Input, NgZone, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵprovideZonelessChangeDetection as provideZonelessChangeDetection} from '@angular/core';
|
||||
import {toSignal} from '@angular/core/rxjs-interop';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BehaviorSubject, firstValueFrom} from 'rxjs';
|
||||
import {filter} from 'rxjs/operators';
|
||||
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
|
||||
private appRef = inject(ApplicationRef);
|
||||
private taskService = inject(PendingTasks);
|
||||
private pendingRenderTaskId: number|null = null;
|
||||
|
||||
notify(): void {
|
||||
if (this.pendingRenderTaskId !== null) return;
|
||||
|
||||
this.pendingRenderTaskId = this.taskService.add();
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (!this.appRef.destroyed) {
|
||||
this.appRef.tick();
|
||||
}
|
||||
} finally {
|
||||
this.taskService.remove(this.pendingRenderTaskId!);
|
||||
this.pendingRenderTaskId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
import {filter, take, tap} from 'rxjs/operators';
|
||||
|
||||
describe('Angular with NoopNgZone', () => {
|
||||
function whenStable(): Promise<boolean> {
|
||||
return firstValueFrom(TestBed.inject(EnvironmentInjector).get(ApplicationRef)
|
||||
.isStable.pipe(filter(stable => stable)));
|
||||
return firstValueFrom(TestBed.inject(EnvironmentInjector)
|
||||
.get(ApplicationRef)
|
||||
.isStable.pipe(filter(stable => stable)));
|
||||
}
|
||||
|
||||
function isStable(): boolean {
|
||||
|
|
@ -60,42 +35,33 @@ describe('Angular with NoopNgZone', () => {
|
|||
}
|
||||
|
||||
describe('notifies scheduler', () => {
|
||||
let scheduler: ChangeDetectionSchedulerImpl;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: NgZone, useClass: ɵNoopNgZone},
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
|
||||
]
|
||||
TestBed.configureTestingModule({providers: [provideZonelessChangeDetection()]});
|
||||
});
|
||||
|
||||
it('contributes to application stableness', async () => {
|
||||
const val = signal('initial');
|
||||
@Component({template: '{{val()}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = val;
|
||||
}
|
||||
const environmentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const component = createComponent(TestComponent, {environmentInjector});
|
||||
const appRef = environmentInjector.get(ApplicationRef);
|
||||
|
||||
appRef.attachView(component.hostView);
|
||||
expect(isStable()).toBeFalse();
|
||||
|
||||
// Cause another pending CD immediately after render and verify app has not stabilized
|
||||
await whenStable().then(() => {
|
||||
val.set('new');
|
||||
});
|
||||
scheduler = TestBed.inject(ChangeDetectionSchedulerImpl);
|
||||
expect(isStable()).toBeFalse();
|
||||
|
||||
await whenStable();
|
||||
expect(isStable()).toBeTrue();
|
||||
});
|
||||
|
||||
it('contributes to application stableness', async () => {
|
||||
const val = signal('initial');
|
||||
@Component({template: '{{val()}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = val;
|
||||
}
|
||||
const environmentInjector = TestBed.inject(EnvironmentInjector);
|
||||
const component = createComponent(TestComponent, {environmentInjector});
|
||||
const appRef = environmentInjector.get(ApplicationRef);
|
||||
|
||||
appRef.attachView(component.hostView);
|
||||
expect(isStable()).toBeFalse();
|
||||
|
||||
// Cause another pending CD immediately after render and verify app has not stabilized
|
||||
await whenStable().then(() => {
|
||||
val.set('new');
|
||||
});
|
||||
expect(isStable()).toBeFalse();
|
||||
|
||||
await whenStable();
|
||||
expect(isStable()).toBeTrue();
|
||||
});
|
||||
|
||||
it('when signal updates', async () => {
|
||||
const val = signal('initial');
|
||||
@Component({template: '{{val()}}', standalone: true})
|
||||
|
|
@ -323,6 +289,29 @@ describe('Angular with NoopNgZone', () => {
|
|||
appRef.attachView(component.hostView);
|
||||
expect(isStable()).toBe(true);
|
||||
});
|
||||
|
||||
it('when a stable subscription synchronously causes another notification', async () => {
|
||||
const val = signal('initial');
|
||||
@Component({template: '{{val()}}', standalone: true})
|
||||
class TestComponent {
|
||||
val = val;
|
||||
}
|
||||
|
||||
const component = await createAndAttachComponent(TestComponent);
|
||||
expect(component.location.nativeElement.innerText).toEqual('initial');
|
||||
|
||||
val.set('new');
|
||||
await TestBed.inject(ApplicationRef)
|
||||
.isStable
|
||||
.pipe(
|
||||
filter(stable => stable),
|
||||
take(1),
|
||||
tap(() => val.set('newer')),
|
||||
)
|
||||
.toPromise();
|
||||
await whenStable();
|
||||
expect(component.location.nativeElement.innerText).toEqual('newer');
|
||||
});
|
||||
});
|
||||
|
||||
it('can recover when an error is re-thrown by the ErrorHandler', async () => {
|
||||
|
|
@ -341,9 +330,7 @@ describe('Angular with NoopNgZone', () => {
|
|||
}
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: NgZone, useClass: ɵNoopNgZone},
|
||||
{provide: PLATFORM_ID, useValue: PLATFORM_BROWSER_ID},
|
||||
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
|
||||
provideZonelessChangeDetection(),
|
||||
{
|
||||
provide: ErrorHandler, useClass: class extends ErrorHandler {
|
||||
override handleError(error: any): void {
|
||||
|
|
|
|||
Loading…
Reference in a new issue