From e6425f7bc1306d01a3ce0915b0455300a0bb9cfb Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 16 Apr 2024 11:29:24 -0700 Subject: [PATCH] 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 --- .../scheduling/zoneless_scheduling_impl.ts | 20 +++++++++---------- packages/core/src/util/callback_scheduler.ts | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts index 12324e83a84..90e848cf788 100644 --- a/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts +++ b/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts @@ -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 { diff --git a/packages/core/src/util/callback_scheduler.ts b/packages/core/src/util/callback_scheduler.ts index 4a53f0d8140..930ab6e6ff6 100644 --- a/packages/core/src/util/callback_scheduler.ts +++ b/packages/core/src/util/callback_scheduler.ts @@ -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;