refactor(core): use patched timers in root zone for zoneless scheduler (#55367)

Rather than attempting to use the native timing functions, this commit
simplifies the logic significantly by using the global timer functions
as they are, either patched or unpatched. When Zone is defined, we run
the timers in the root zone. This has more predictable behavior and
timing than (a) using both patched and unpatched versions of timers in
different places (b) trying to get an unpatched timer and failing due to
environment specifics and patches that aren't ZoneJS.

We will try to update the coalescing behavior of ZoneJS in a future PR
to also just use the patched version of the timers instead of the
"fakeTopEvent" workaround with the native timers.

PR Close #55367
This commit is contained in:
Andrew Scott 2024-04-16 11:29:24 -07:00 committed by Alex Rickabaugh
parent f09c5a7bc4
commit e6425f7bc1
2 changed files with 12 additions and 12 deletions

View file

@ -30,7 +30,7 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
private readonly zonelessEnabled = inject(ZONELESS_ENABLED);
private readonly disableScheduling =
inject(ZONELESS_SCHEDULER_DISABLED, {optional: true}) ?? false;
private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.scheduleEventTask;
private readonly zoneIsDefined = typeof Zone !== 'undefined' && !!Zone.root.run;
private readonly afterTickSubscription = this.appRef.afterTick.subscribe(() => {
// If the scheduler isn't running a tick but the application ticked, that means
// someone called ApplicationRef.tick manually. In this case, we should cancel
@ -60,17 +60,17 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
}
this.pendingRenderTaskId = this.taskService.add();
this.cancelScheduledCallback = scheduleCallback(() => {
if (this.zoneIsDefined) {
// https://github.com/angular/angular/blob/c9abe775d07d075b171a187844d09e57f9685f3b/packages/core/src/zone/ng_zone.ts#L387-L395
const task = Zone.root.scheduleEventTask('fakeTopEventTask', () => {
if (this.zoneIsDefined) {
Zone.root.run(() => {
this.cancelScheduledCallback = scheduleCallback(() => {
this.tick(this.shouldRefreshViews);
}, undefined, () => {}, () => {});
task.invoke();
} else {
}, false /** useNativeTimers */);
});
} else {
this.cancelScheduledCallback = scheduleCallback(() => {
this.tick(this.shouldRefreshViews);
}
});
}, false /** useNativeTimers */);
}
}
private shouldScheduleTick(): boolean {

View file

@ -34,7 +34,7 @@ import {global} from './global';
*
* @returns a function to cancel the scheduled callback
*/
export function scheduleCallback(callback: Function): () => void {
export function scheduleCallback(callback: Function, useNativeTimers = true): () => void {
// Note: the `scheduleCallback` is used in the `NgZone` class, but we cannot use the
// `inject` function. The `NgZone` instance may be created manually, and thus the injection
// context will be unavailable. This might be enough to check whether `requestAnimationFrame` is
@ -43,7 +43,7 @@ export function scheduleCallback(callback: Function): () => void {
let nativeRequestAnimationFrame =
hasRequestAnimationFrame ? global['requestAnimationFrame'] : null;
let nativeSetTimeout = global['setTimeout'];
if (typeof Zone !== 'undefined') {
if (typeof Zone !== 'undefined' && useNativeTimers) {
if (hasRequestAnimationFrame) {
nativeRequestAnimationFrame =
global[Zone.__symbol__('requestAnimationFrame')] ?? nativeRequestAnimationFrame;