Skip to content

Commit 6789c7e

Browse files
mmalerbaatscott
authored andcommittedFeb 12, 2025·
fix(core): Defer afterRender until after first CD (#59455) (#59551)
This reverts commit ac2dbe3. PR Close #59551
1 parent c87e581 commit 6789c7e

24 files changed

+197
-137
lines changed
 

‎packages/core/src/application/application_ref.ts

-15
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,6 @@ export class ApplicationRef {
299299
*/
300300
dirtyFlags = ApplicationRefDirtyFlags.None;
301301

302-
/**
303-
* Like `dirtyFlags` but don't cause `tick()` to loop.
304-
*
305-
* @internal
306-
*/
307-
deferredDirtyFlags = ApplicationRefDirtyFlags.None;
308-
309302
/**
310303
* Most recent snapshot from the `TracingService`, if any.
311304
*
@@ -634,10 +627,6 @@ export class ApplicationRef {
634627
this._rendererFactory = this._injector.get(RendererFactory2, null, {optional: true});
635628
}
636629

637-
// When beginning synchronization, all deferred dirtiness becomes active dirtiness.
638-
this.dirtyFlags |= this.deferredDirtyFlags;
639-
this.deferredDirtyFlags = ApplicationRefDirtyFlags.None;
640-
641630
let runs = 0;
642631
while (this.dirtyFlags !== ApplicationRefDirtyFlags.None && runs++ < MAXIMUM_REFRESH_RERUNS) {
643632
profiler(ProfilerEvent.ChangeDetectionSyncStart);
@@ -660,10 +649,6 @@ export class ApplicationRef {
660649
* Perform a single synchronization pass.
661650
*/
662651
private synchronizeOnce(): void {
663-
// If we happened to loop, deferred dirtiness can be processed as active dirtiness again.
664-
this.dirtyFlags |= this.deferredDirtyFlags;
665-
this.deferredDirtyFlags = ApplicationRefDirtyFlags.None;
666-
667652
// First, process any dirty root effects.
668653
if (this.dirtyFlags & ApplicationRefDirtyFlags.RootEffects) {
669654
this.dirtyFlags &= ~ApplicationRefDirtyFlags.RootEffects;

‎packages/core/src/change_detection/scheduling/zoneless_scheduling.ts

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export const enum NotificationSource {
3333
// but we should execute render hooks:
3434
// Render hooks are guaranteed to execute with the schedulers timing.
3535
RenderHook,
36-
DeferredRenderHook,
3736
// Views might be created outside and manipulated in ways that
3837
// we cannot be aware of. When a view is attached, Angular now "knows"
3938
// about it and we now know that DOM might have changed (and we should

‎packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts

+2-9
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import {NgZone, NgZonePrivate, NoopNgZone, angularZoneInstanceIdProperty} from '
2525
import {
2626
ChangeDetectionScheduler,
2727
NotificationSource,
28-
ZONELESS_ENABLED,
2928
PROVIDED_ZONELESS,
30-
ZONELESS_SCHEDULER_DISABLED,
3129
SCHEDULE_IN_ROOT_ZONE,
30+
ZONELESS_ENABLED,
31+
ZONELESS_SCHEDULER_DISABLED,
3232
} from './zoneless_scheduling';
3333
import {TracingService} from '../../application/tracing';
3434

@@ -140,13 +140,6 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
140140
this.appRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeCheck;
141141
break;
142142
}
143-
case NotificationSource.DeferredRenderHook: {
144-
// Render hooks are "deferred" when they're triggered from other render hooks. Using the
145-
// deferred dirty flags ensures that adding new hooks doesn't automatically trigger a loop
146-
// inside tick().
147-
this.appRef.deferredDirtyFlags |= ApplicationRefDirtyFlags.AfterRender;
148-
break;
149-
}
150143
case NotificationSource.CustomElement: {
151144
// We use `ViewTreeTraversal` to ensure we refresh the element even if this is triggered
152145
// during CD. In practice this is a no-op since the elements code also calls via a

‎packages/core/src/defer/dom_triggers.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {afterNextRender} from '../render3/after_render/hooks';
109
import type {Injector} from '../di';
10+
import {AfterRenderRef} from '../render3/after_render/api';
11+
import {afterRender} from '../render3/after_render/hooks';
1112
import {assertLContainer, assertLView} from '../render3/assert';
1213
import {CONTAINER_HEADER_OFFSET} from '../render3/interfaces/container';
1314
import {TNode} from '../render3/interfaces/node';
@@ -280,9 +281,11 @@ export function registerDomTrigger(
280281
) {
281282
const injector = initialLView[INJECTOR];
282283
const zone = injector.get(NgZone);
284+
let poll: AfterRenderRef;
283285
function pollDomTrigger() {
284286
// If the initial view was destroyed, we don't need to do anything.
285287
if (isDestroyed(initialLView)) {
288+
poll.destroy();
286289
return;
287290
}
288291

@@ -294,17 +297,20 @@ export function registerDomTrigger(
294297
renderedState !== DeferBlockInternalState.Initial &&
295298
renderedState !== DeferBlockState.Placeholder
296299
) {
300+
poll.destroy();
297301
return;
298302
}
299303

300304
const triggerLView = getTriggerLView(initialLView, tNode, walkUpTimes);
301305

302306
// Keep polling until we resolve the trigger's LView.
303307
if (!triggerLView) {
304-
afterNextRender({read: pollDomTrigger}, {injector});
308+
// Keep polling.
305309
return;
306310
}
307311

312+
poll.destroy();
313+
308314
// It's possible that the trigger's view was destroyed before we resolved the trigger element.
309315
if (isDestroyed(triggerLView)) {
310316
return;
@@ -339,5 +345,5 @@ export function registerDomTrigger(
339345
}
340346

341347
// Begin polling for the trigger.
342-
afterNextRender({read: pollDomTrigger}, {injector});
348+
poll = afterRender({read: pollDomTrigger}, {injector});
343349
}

‎packages/core/src/render3/after_render/hooks.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {inject} from '../../di/injector_compatibility';
1313
import {DestroyRef} from '../../linker/destroy_ref';
1414
import {performanceMarkFeature} from '../../util/performance';
1515
import {assertNotInReactiveContext} from '../reactivity/asserts';
16+
import {ViewContext} from '../view_context';
1617
import {AfterRenderPhase, AfterRenderRef} from './api';
1718
import {
1819
AfterRenderHooks,
@@ -459,9 +460,11 @@ function afterRenderImpl(
459460

460461
const hooks = options?.phase ?? AfterRenderPhase.MixedReadWrite;
461462
const destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
463+
const viewContext = injector.get(ViewContext, null, {optional: true});
462464
const sequence = new AfterRenderSequence(
463465
manager.impl,
464466
getHooks(callbackOrSpec, hooks),
467+
viewContext?.view,
465468
once,
466469
destroyRef,
467470
tracing?.snapshot(null),

‎packages/core/src/render3/after_render/manager.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {AfterRenderPhase, AfterRenderRef} from './api';
10-
import {NgZone} from '../../zone';
11-
import {inject} from '../../di/injector_compatibility';
12-
import {ɵɵdefineInjectable} from '../../di/interface/defs';
13-
import {ErrorHandler} from '../../error_handler';
9+
import {TracingAction, TracingService, TracingSnapshot} from '../../application/tracing';
1410
import {
1511
ChangeDetectionScheduler,
1612
NotificationSource,
1713
} from '../../change_detection/scheduling/zoneless_scheduling';
14+
import {inject} from '../../di/injector_compatibility';
15+
import {ɵɵdefineInjectable} from '../../di/interface/defs';
16+
import {ErrorHandler} from '../../error_handler';
1817
import {type DestroyRef} from '../../linker/destroy_ref';
19-
import {TracingAction, TracingService, TracingSnapshot} from '../../application/tracing';
18+
import {NgZone} from '../../zone';
19+
import {AFTER_RENDER_SEQUENCES_TO_ADD, FLAGS, LView, LViewFlags} from '../interfaces/view';
2020
import {profiler} from '../profiler';
2121
import {ProfilerEvent} from '../profiler_types';
22+
import {markAncestorsForTraversal} from '../util/view_utils';
23+
import {AfterRenderPhase, AfterRenderRef} from './api';
2224

2325
export class AfterRenderManager {
2426
impl: AfterRenderImpl | null = null;
@@ -111,7 +113,7 @@ export class AfterRenderImpl {
111113
this.sequences.add(sequence);
112114
}
113115
if (this.deferredRegistrations.size > 0) {
114-
this.scheduler.notify(NotificationSource.DeferredRenderHook);
116+
this.scheduler.notify(NotificationSource.RenderHook);
115117
}
116118
this.deferredRegistrations.clear();
117119

@@ -121,16 +123,28 @@ export class AfterRenderImpl {
121123
}
122124

123125
register(sequence: AfterRenderSequence): void {
124-
if (!this.executing) {
125-
this.sequences.add(sequence);
126-
// Trigger an `ApplicationRef.tick()` if one is not already pending/running, because we have a
127-
// new render hook that needs to run.
128-
this.scheduler.notify(NotificationSource.RenderHook);
126+
const {view} = sequence;
127+
if (view !== undefined) {
128+
// Delay adding it to the manager, add it to the view instead.
129+
(view[AFTER_RENDER_SEQUENCES_TO_ADD] ??= []).push(sequence);
130+
131+
// Mark the view for traversal to ensure we eventually schedule the afterNextRender.
132+
markAncestorsForTraversal(view);
133+
view[FLAGS] |= LViewFlags.HasChildViewsToRefresh;
134+
} else if (!this.executing) {
135+
this.addSequence(sequence);
129136
} else {
130137
this.deferredRegistrations.add(sequence);
131138
}
132139
}
133140

141+
addSequence(sequence: AfterRenderSequence): void {
142+
this.sequences.add(sequence);
143+
// Trigger an `ApplicationRef.tick()` if one is not already pending/running, because we have a
144+
// new render hook that needs to run.
145+
this.scheduler.notify(NotificationSource.RenderHook);
146+
}
147+
134148
unregister(sequence: AfterRenderSequence): void {
135149
if (this.executing && this.sequences.has(sequence)) {
136150
// We can't remove an `AfterRenderSequence` in the middle of iteration.
@@ -185,6 +199,7 @@ export class AfterRenderSequence implements AfterRenderRef {
185199
constructor(
186200
readonly impl: AfterRenderImpl,
187201
readonly hooks: AfterRenderHooks,
202+
readonly view: LView | undefined,
188203
public once: boolean,
189204
destroyRef: DestroyRef | null,
190205
public snapshot: TracingSnapshot | null = null,
@@ -207,5 +222,9 @@ export class AfterRenderSequence implements AfterRenderRef {
207222
destroy(): void {
208223
this.impl.unregister(this);
209224
this.unregisterOnDestroy?.();
225+
const scheduled = this.view?.[AFTER_RENDER_SEQUENCES_TO_ADD];
226+
if (scheduled) {
227+
this.view[AFTER_RENDER_SEQUENCES_TO_ADD] = scheduled.filter((s) => s !== this);
228+
}
210229
}
211230
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {AFTER_RENDER_SEQUENCES_TO_ADD, LView} from '../interfaces/view';
10+
11+
export function addAfterRenderSequencesForView(lView: LView) {
12+
if (lView[AFTER_RENDER_SEQUENCES_TO_ADD] !== null) {
13+
for (const sequence of lView[AFTER_RENDER_SEQUENCES_TO_ADD]) {
14+
sequence.impl.addSequence(sequence);
15+
}
16+
lView[AFTER_RENDER_SEQUENCES_TO_ADD].length = 0;
17+
}
18+
}

‎packages/core/src/render3/instructions/change_detection.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717

1818
import {RuntimeError, RuntimeErrorCode} from '../../errors';
1919
import {assertDefined, assertEqual} from '../../util/assert';
20+
import {addAfterRenderSequencesForView} from '../after_render/view';
2021
import {executeCheckHooks, executeInitAndCheckHooks, incrementInitPhaseFlags} from '../hooks';
2122
import {CONTAINER_HEADER_OFFSET, LContainerFlags, MOVED_VIEWS} from '../interfaces/container';
2223
import {ComponentTemplate, HostBindingsFunction, RenderFlags} from '../interfaces/definition';
@@ -33,8 +34,8 @@ import {
3334
TView,
3435
} from '../interfaces/view';
3536
import {
36-
getOrCreateTemporaryConsumer,
3737
getOrBorrowReactiveLViewConsumer,
38+
getOrCreateTemporaryConsumer,
3839
maybeReturnReactiveLViewConsumer,
3940
ReactiveLViewConsumer,
4041
viewShouldHaveReactiveConsumer,
@@ -64,11 +65,11 @@ import {
6465
} from '../util/view_utils';
6566

6667
import {isDestroyed} from '../interfaces/type_checks';
67-
import {ProfilerEvent} from '../profiler_types';
6868
import {profiler} from '../profiler';
69+
import {ProfilerEvent} from '../profiler_types';
70+
import {executeViewQueryFn, refreshContentQueries} from '../queries/query_execution';
6971
import {runEffectsInView} from '../reactivity/view_effect_runner';
7072
import {executeTemplate, handleError} from './shared';
71-
import {executeViewQueryFn, refreshContentQueries} from '../queries/query_execution';
7273

7374
/**
7475
* The maximum number of times the change detection traversal will rerun before throwing an error.
@@ -354,6 +355,8 @@ export function refreshView<T>(
354355
// no changes cycle, the component would be not be dirty for the next update pass. This would
355356
// be different in production mode where the component dirty state is not reset.
356357
if (!isInCheckNoChangesPass) {
358+
addAfterRenderSequencesForView(lView);
359+
357360
lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass);
358361
}
359362
} catch (e) {
@@ -506,6 +509,9 @@ function detectChangesInView(lView: LView, mode: ChangeDetectionMode) {
506509
if (components !== null) {
507510
detectChangesInChildComponents(lView, components, ChangeDetectionMode.Targeted);
508511
}
512+
if (!isInCheckNoChangesPass) {
513+
addAfterRenderSequencesForView(lView);
514+
}
509515
}
510516
}
511517

‎packages/core/src/render3/interfaces/view.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {ProviderToken} from '../../di/provider_token';
1313
import {DehydratedView} from '../../hydration/interfaces';
1414
import {SchemaMetadata} from '../../metadata/schema';
1515
import {Sanitizer} from '../../sanitization/sanitizer';
16+
import type {AfterRenderSequence} from '../after_render/manager';
1617
import type {ReactiveLViewConsumer} from '../reactive_lview_consumer';
1718
import type {ViewEffectNode} from '../reactivity/effect';
1819

@@ -67,6 +68,7 @@ export const ON_DESTROY_HOOKS = 21;
6768
export const EFFECTS_TO_SCHEDULE = 22;
6869
export const EFFECTS = 23;
6970
export const REACTIVE_TEMPLATE_CONSUMER = 24;
71+
export const AFTER_RENDER_SEQUENCES_TO_ADD = 25;
7072

7173
/**
7274
* Size of LView's header. Necessary to adjust for it when setting slots.
@@ -75,7 +77,7 @@ export const REACTIVE_TEMPLATE_CONSUMER = 24;
7577
* instruction index into `LView` index. All other indexes should be in the `LView` index space and
7678
* there should be no need to refer to `HEADER_OFFSET` anywhere else.
7779
*/
78-
export const HEADER_OFFSET = 25;
80+
export const HEADER_OFFSET = 26;
7981

8082
// This interface replaces the real LView interface if it is an arg or a
8183
// return value of a public instruction. This ensures we don't need to expose
@@ -362,6 +364,9 @@ export interface LView<T = unknown> extends Array<any> {
362364
* if any signals were read.
363365
*/
364366
[REACTIVE_TEMPLATE_CONSUMER]: ReactiveLViewConsumer | null;
367+
368+
// AfterRenderSequences that need to be scheduled
369+
[AFTER_RENDER_SEQUENCES_TO_ADD]: AfterRenderSequence[] | null;
365370
}
366371

367372
/**

‎packages/core/src/render3/reactivity/after_render_effect.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,26 @@ import {
1919
import {type Signal} from '../reactivity/api';
2020
import {type EffectCleanupFn, type EffectCleanupRegisterFn} from './effect';
2121

22+
import {TracingService, TracingSnapshot} from '../../application/tracing';
2223
import {
2324
ChangeDetectionScheduler,
2425
NotificationSource,
2526
} from '../../change_detection/scheduling/zoneless_scheduling';
27+
import {assertInInjectionContext} from '../../di/contextual';
2628
import {Injector} from '../../di/injector';
2729
import {inject} from '../../di/injector_compatibility';
30+
import {DestroyRef} from '../../linker/destroy_ref';
31+
import {AfterRenderPhase, type AfterRenderRef} from '../after_render/api';
32+
import {NOOP_AFTER_RENDER_REF, type AfterRenderOptions} from '../after_render/hooks';
2833
import {
2934
AFTER_RENDER_PHASES,
3035
AfterRenderImpl,
3136
AfterRenderManager,
3237
AfterRenderSequence,
3338
} from '../after_render/manager';
34-
import {AfterRenderPhase, type AfterRenderRef} from '../after_render/api';
35-
import {NOOP_AFTER_RENDER_REF, type AfterRenderOptions} from '../after_render/hooks';
36-
import {DestroyRef} from '../../linker/destroy_ref';
39+
import {LView} from '../interfaces/view';
40+
import {ViewContext} from '../view_context';
3741
import {assertNotInReactiveContext} from './asserts';
38-
import {assertInInjectionContext} from '../../di/contextual';
39-
import {TracingService, TracingSnapshot} from '../../application/tracing';
4042

4143
const NOT_SET = Symbol('NOT_SET');
4244
const EMPTY_CLEANUP_SET = new Set<() => void>();
@@ -174,13 +176,14 @@ class AfterRenderEffectSequence extends AfterRenderSequence {
174176
constructor(
175177
impl: AfterRenderImpl,
176178
effectHooks: Array<AfterRenderPhaseEffectHook | undefined>,
179+
view: LView | undefined,
177180
readonly scheduler: ChangeDetectionScheduler,
178181
destroyRef: DestroyRef,
179182
snapshot: TracingSnapshot | null = null,
180183
) {
181184
// Note that we also initialize the underlying `AfterRenderSequence` hooks to `undefined` and
182185
// populate them as we create reactive nodes below.
183-
super(impl, [undefined, undefined, undefined, undefined], false, destroyRef, snapshot);
186+
super(impl, [undefined, undefined, undefined, undefined], view, false, destroyRef, snapshot);
184187

185188
// Setup a reactive node for each phase.
186189
for (const phase of AFTER_RENDER_PHASES) {
@@ -380,9 +383,12 @@ export function afterRenderEffect<E = never, W = never, M = never>(
380383
spec = {mixedReadWrite: callbackOrSpec as any};
381384
}
382385

386+
const viewContext = injector.get(ViewContext, null, {optional: true});
387+
383388
const sequence = new AfterRenderEffectSequence(
384389
manager.impl,
385390
[spec.earlyRead, spec.write, spec.mixedReadWrite, spec.read] as AfterRenderPhaseEffectHook[],
391+
viewContext?.view,
386392
scheduler,
387393
injector.get(DestroyRef),
388394
tracing?.snapshot(null),

0 commit comments

Comments
 (0)
Please sign in to comment.