mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(core): Ensure idle scheduler batching accounts for change detection
This updates the deadline check in requestIdleCallback to ensure change detection properly dirties the views.
This commit is contained in:
committed by
Jessica Janiuk
parent
9bb4f02bef
commit
e1bc342856
@@ -149,12 +149,12 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler, O
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
case NotificationSource.MarkAncestorsForTraversal: {
|
||||
case NotificationSource.MarkAncestorsForTraversal:
|
||||
case NotificationSource.DeferBlockStateUpdate: {
|
||||
this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeTraversal;
|
||||
break;
|
||||
}
|
||||
case NotificationSource.DebugApplyChanges:
|
||||
case NotificationSource.DeferBlockStateUpdate:
|
||||
case NotificationSource.MarkForCheck:
|
||||
case NotificationSource.Listener:
|
||||
case NotificationSource.SetInput: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {OnDestroy} from '../core';
|
||||
import {Injector, inject, ɵɵdefineInjectable} from '../di';
|
||||
import {NgZone} from '../zone';
|
||||
import {IDLE_SERVICE} from './idle_service';
|
||||
import {ApplicationRef} from '../application/application_ref';
|
||||
|
||||
/**
|
||||
* Helper function to schedule a callback to be invoked when a browser becomes idle.
|
||||
@@ -35,6 +36,7 @@ export class IdleScheduler implements OnDestroy {
|
||||
|
||||
// Queue of callbacks to be invoked next.
|
||||
queue = new Set<VoidFunction>();
|
||||
applicationRef = inject(ApplicationRef);
|
||||
|
||||
ngZone = inject(NgZone);
|
||||
private readonly idleService = inject(IDLE_SERVICE);
|
||||
@@ -64,6 +66,10 @@ export class IdleScheduler implements OnDestroy {
|
||||
|
||||
for (const callbackFn of this.queue) {
|
||||
callbackFn();
|
||||
// _tick here is an optimized change detection check and is safe to call here.
|
||||
// We also account for the time it takes to run change detection
|
||||
// for the newly-created view as a part of the same idle callback.
|
||||
this.applicationRef._tick();
|
||||
this.queue.delete(callbackFn);
|
||||
|
||||
if (deadline && deadline.timeRemaining() === 0 && !deadline.didTimeout) {
|
||||
|
||||
@@ -15,13 +15,12 @@ import {
|
||||
} from '../hydration/interfaces';
|
||||
import {assertLContainer, assertTNodeForLView} from '../render3/assert';
|
||||
import {ChainedInjector} from '../render3/chained_injector';
|
||||
import {markViewDirty} from '../render3/instructions/mark_view_dirty';
|
||||
import {handleUncaughtError} from '../render3/instructions/shared';
|
||||
import {DEHYDRATED_VIEWS, LContainer} from '../render3/interfaces/container';
|
||||
import {TContainerNode, TNode} from '../render3/interfaces/node';
|
||||
import {isDestroyed} from '../render3/interfaces/type_checks';
|
||||
import {HEADER_OFFSET, INJECTOR, LView, PARENT, TVIEW, TView} from '../render3/interfaces/view';
|
||||
import {getConstant, getTNode} from '../render3/util/view_utils';
|
||||
import {markViewForRefresh, getConstant, getTNode} from '../render3/util/view_utils';
|
||||
import {createAndRenderEmbeddedLView, shouldAddViewToDom} from '../render3/view_manipulation';
|
||||
import {assertDefined} from '../util/assert';
|
||||
|
||||
@@ -289,7 +288,7 @@ function applyDeferBlockState(
|
||||
viewIndex,
|
||||
shouldAddViewToDom(activeBlockTNode, dehydratedView),
|
||||
);
|
||||
markViewDirty(embeddedLView, NotificationSource.DeferBlockStateUpdate);
|
||||
markViewForRefresh(embeddedLView);
|
||||
|
||||
if (dehydratedViewIx > -1) {
|
||||
// Erase dehydrated view info in a given LContainer, so that the view is not
|
||||
|
||||
@@ -701,6 +701,86 @@ describe('@defer', () => {
|
||||
// Expect that the loading resources function was not invoked again (counter remains 1).
|
||||
expect(loadingFnInvokedTimes).toBe(1);
|
||||
});
|
||||
|
||||
it('should trigger change detection when `on idle` is fired without explicit fixture.detectChanges()', async () => {
|
||||
@Component({
|
||||
selector: 'nested-cmp',
|
||||
template: 'Rendering {{ block }} block.',
|
||||
})
|
||||
class NestedCmp {
|
||||
@Input() block!: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'root-app',
|
||||
imports: [NestedCmp],
|
||||
template: `
|
||||
@defer (on idle) {
|
||||
<nested-cmp [block]="'primary'" />
|
||||
} @placeholder {
|
||||
Placeholder
|
||||
} @loading {
|
||||
Loading
|
||||
}
|
||||
`,
|
||||
})
|
||||
class RootCmp {}
|
||||
|
||||
let loadingFnInvokedTimes = 0;
|
||||
const deferDepsInterceptor = {
|
||||
intercept() {
|
||||
return () => {
|
||||
loadingFnInvokedTimes++;
|
||||
return [dynamicImportOf(NestedCmp)];
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const idleCallbacks: IdleRequestCallback[] = [];
|
||||
const mockRequestIdleCallback = (
|
||||
callback: IdleRequestCallback,
|
||||
options?: IdleRequestOptions,
|
||||
): number => {
|
||||
idleCallbacks.push(callback);
|
||||
return 1;
|
||||
};
|
||||
|
||||
const nativeRequestIdleCallback = globalThis.requestIdleCallback;
|
||||
const nativeCancelIdleCallback = globalThis.cancelIdleCallback;
|
||||
globalThis.requestIdleCallback = mockRequestIdleCallback;
|
||||
globalThis.cancelIdleCallback = (id: number) => {};
|
||||
|
||||
try {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
...COMMON_PROVIDERS,
|
||||
{provide: ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR, useValue: deferDepsInterceptor},
|
||||
],
|
||||
});
|
||||
|
||||
clearDirectiveDefs(RootCmp);
|
||||
|
||||
const fixture = TestBed.createComponent(RootCmp);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Placeholder');
|
||||
expect(loadingFnInvokedTimes).toBe(0);
|
||||
|
||||
// Trigger the idle callback
|
||||
expect(idleCallbacks.length).toBe(1);
|
||||
idleCallbacks[0]({timeRemaining: () => 50, didTimeout: false} as IdleDeadline);
|
||||
|
||||
// Ensure that loading function was called
|
||||
expect(loadingFnInvokedTimes).toBe(1);
|
||||
|
||||
// The tick generated from the defer block state change inside the idle scheduler
|
||||
// should have updated the view automatically, showing the loading block.
|
||||
expect(fixture.nativeElement.outerHTML).toContain('Loading');
|
||||
} finally {
|
||||
globalThis.requestIdleCallback = nativeRequestIdleCallback;
|
||||
globalThis.cancelIdleCallback = nativeCancelIdleCallback;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('directive matching', () => {
|
||||
|
||||
Reference in New Issue
Block a user