Skip to content

Commit

Permalink
refactor(core): Update coalescing to just use patched timers in root …
Browse files Browse the repository at this point in the history
…zone

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.
  • Loading branch information
atscott committed May 2, 2024
1 parent a0ec2d8 commit 128900b
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
Zone.root.run(() => {
this.cancelScheduledCallback = scheduleCallback(() => {
this.tick(this.shouldRefreshViews);
}, false /** useNativeTimers */);
});
});
} else {
this.cancelScheduledCallback = scheduleCallback(() => {
this.tick(this.shouldRefreshViews);
}, false /** useNativeTimers */);
});
}
}

Expand Down
40 changes: 11 additions & 29 deletions packages/core/src/util/callback_scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,42 +34,24 @@ import {global} from './global';
*
* @returns a function to cancel the scheduled callback
*/
export function scheduleCallbackWithRafRace(
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
// available because otherwise, we'll fall back to `setTimeout`.
const hasRequestAnimationFrame = typeof global['requestAnimationFrame'] === 'function';
let nativeRequestAnimationFrame = hasRequestAnimationFrame
? global['requestAnimationFrame']
: null;
let nativeSetTimeout = global['setTimeout'];
if (typeof Zone !== 'undefined' && useNativeTimers) {
if (hasRequestAnimationFrame) {
nativeRequestAnimationFrame =
global[Zone.__symbol__('requestAnimationFrame')] ?? nativeRequestAnimationFrame;
}
nativeSetTimeout = global[Zone.__symbol__('setTimeout')] ?? nativeSetTimeout;
}

export function scheduleCallbackWithRafRace(callback: Function): () => void {
let executeCallback = true;
nativeSetTimeout(() => {
if (!executeCallback) {
return;
}
executeCallback = false;
callback();
});
nativeRequestAnimationFrame?.(() => {
setTimeout(() => {
if (!executeCallback) {
return;
}
executeCallback = false;
callback();
});
if (typeof global['requestAnimationFrame'] === 'function') {
global['requestAnimationFrame'](() => {
if (!executeCallback) {
return;
}
executeCallback = false;
callback();
});
}

return () => {
executeCallback = false;
Expand Down
41 changes: 8 additions & 33 deletions packages/core/src/zone/ng_zone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ export class NgZone {
!shouldCoalesceRunChangeDetection && shouldCoalesceEventChangeDetection;
self.shouldCoalesceRunChangeDetection = shouldCoalesceRunChangeDetection;
self.callbackScheduled = false;
self.scheduleCallback = scheduleCallbackWithRafRace;
forkInnerZoneWithAngularBehavior(self);
}

Expand Down Expand Up @@ -326,12 +325,6 @@ interface NgZonePrivate extends NgZone {
*
*/
shouldCoalesceRunChangeDetection: boolean;

scheduleCallback: (callback: Function) => void;

// Cache a "fake" top eventTask so you don't need to schedule a new task every
// time you run a `checkStable`.
fakeTopEventTask: Task;
}

function checkStable(zone: NgZonePrivate) {
Expand Down Expand Up @@ -385,32 +378,14 @@ function delayChangeDetectionForEvents(zone: NgZonePrivate) {
return;
}
zone.callbackScheduled = true;
zone.scheduleCallback.call(global, () => {
// This is a work around for https://github.com/angular/angular/issues/36839.
// The core issue is that when event coalescing is enabled it is possible for microtasks
// to get flushed too early (As is the case with `Promise.then`) between the
// coalescing eventTasks.
//
// To workaround this we schedule a "fake" eventTask before we process the
// coalescing eventTasks. The benefit of this is that the "fake" container eventTask
// will prevent the microtasks queue from getting drained in between the coalescing
// eventTask execution.
if (!zone.fakeTopEventTask) {
zone.fakeTopEventTask = Zone.root.scheduleEventTask(
'fakeTopEventTask',
() => {
zone.callbackScheduled = false;
updateMicroTaskStatus(zone);
zone.isCheckStableRunning = true;
checkStable(zone);
zone.isCheckStableRunning = false;
},
undefined,
() => {},
() => {},
);
}
zone.fakeTopEventTask.invoke();
Zone.root.run(() => {
scheduleCallbackWithRafRace(() => {
zone.callbackScheduled = false;
updateMicroTaskStatus(zone);
zone.isCheckStableRunning = true;
checkStable(zone);
zone.isCheckStableRunning = false;
});
});
updateMicroTaskStatus(zone);
}
Expand Down

0 comments on commit 128900b

Please sign in to comment.