Skip to content

Commit

Permalink
fix(core): Avoid refreshing a host view twice when using transplanted…
Browse files Browse the repository at this point in the history
… views

This change fixes and issue where the expectation was that change
detection always goes through `detectChangesInView`. In reality,
`detectChangesInternal` directly calls `refreshView`
and refreshes a view directly without checking if it was dirty (to my discontent).

This update clears the refresh flags at the top of `refreshView` as well
as before traversing child views in `Targeted` mode. The issue only
partially exists with signals in that the flags aren't cleared but we
don't end up refreshing the view a second time because the
`consumerPollProducersForChange` returns `false` the second time we
enter `detectChangesInView`.
  • Loading branch information
atscott committed Nov 28, 2023
1 parent e3dbadd commit dd64c6c
Show file tree
Hide file tree
Showing 14 changed files with 54 additions and 12 deletions.
13 changes: 5 additions & 8 deletions packages/core/src/render3/instructions/change_detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {CONTEXT, EFFECTS_TO_SCHEDULE, ENVIRONMENT, FLAGS, InitPhaseState, LView,
import {getOrBorrowReactiveLViewConsumer, maybeReturnReactiveLViewConsumer, ReactiveLViewConsumer} from '../reactive_lview_consumer';
import {enterView, isInCheckNoChangesMode, leaveView, setBindingIndex, setIsInCheckNoChangesMode} from '../state';
import {getFirstLContainer, getNextLContainer} from '../util/view_traversal_utils';
import {getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, markViewForRefresh, requiresRefreshOrTraversal, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';
import {clearRefreshAndTraversalFlags, getComponentLViewByIndex, isCreationMode, markAncestorsForTraversal, markViewForRefresh, requiresRefreshOrTraversal, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils';

import {executeTemplate, executeViewQueryFn, handleError, processHostBindingOpCodes, refreshContentQueries} from './shared';

Expand Down Expand Up @@ -129,6 +129,8 @@ const enum ChangeDetectionMode {
export function refreshView<T>(
tView: TView, lView: LView, templateFn: ComponentTemplate<{}>|null, context: T) {
ngDevMode && assertEqual(isCreationMode(lView), false, 'Should be run in update mode');
// Remove refresh and traversal flags so that they can be re-dirtied during the refresh process.
clearRefreshAndTraversalFlags(lView);
const flags = lView[FLAGS];
if ((flags & LViewFlags.Destroyed) === LViewFlags.Destroyed) return;

Expand Down Expand Up @@ -399,16 +401,11 @@ function detectChangesInView(lView: LView, mode: ChangeDetectionMode) {
// Refresh views when they have a dirty reactive consumer, regardless of mode.
shouldRefreshView ||= !!(consumer?.dirty && consumerPollProducersForChange(consumer));

// Mark the Flags and `ReactiveNode` as not dirty before refreshing the component, so that they
// can be re-dirtied during the refresh process.
if (consumer) {
consumer.dirty = false;
}
lView[FLAGS] &= ~(LViewFlags.HasChildViewsToRefresh | LViewFlags.RefreshView);

if (shouldRefreshView) {
refreshView(tView, lView, tView.template, lView[CONTEXT]);
} else if (flags & LViewFlags.HasChildViewsToRefresh) {
// Remove refresh and traversal flags so that they can be re-dirtied during the refresh process.
clearRefreshAndTraversalFlags(lView);
detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);
const components = tView.components;
if (components !== null) {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/render3/util/view_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ export function updateAncestorTraversalFlagsOnAttach(lView: LView) {
markAncestorsForTraversal(lView);
}

/**
* Clears flags related to view refresh and traversal (excluding `LViewFlags.Dirty`).
*
* This helper method is meant to be used before refreshing a view. Since `LViewFlags.Dirty` is not
* cleared until the end of view refresh, it is not cleared in this function.
*/
export function clearRefreshAndTraversalFlags(lView: LView) {
const consumer = lView[REACTIVE_TEMPLATE_CONSUMER];
if (consumer) {
consumer.dirty = false;
}
lView[FLAGS] &= ~(LViewFlags.HasChildViewsToRefresh | LViewFlags.RefreshView);
}

/**
* Ensures views above the given `lView` are traversed during change detection even when they are
* not dirty.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,7 @@ describe('change detection for transplanted views', () => {
// Detach view, manually call `detectChanges`, and verify the template was refreshed
component.rootViewContainerRef.detach();
viewRef.detectChanges();
// This view is a backwards reference so it's refreshed twice
expect(component.checks).toEqual(2);
expect(component.checks).toEqual(1);
});

it('should work when change detecting detached transplanted view already marked for refresh',
Expand All @@ -751,8 +750,7 @@ describe('change detection for transplanted views', () => {
// should not affect parent counters.
viewRef.detectChanges();
}).not.toThrow();
// This view is a backwards reference so it's refreshed twice
expect(component.checks).toEqual(2);
expect(component.checks).toEqual(1);
});

it('should work when re-inserting a previously detached transplanted view marked for refresh',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "cloakAndComputeStyles"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "cloakAndComputeStyles"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,9 @@
{
"name": "clearElementContents"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/router/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/test/bundling/todo/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,9 @@
{
"name": "cleanUpView"
},
{
"name": "clearRefreshAndTraversalFlags"
},
{
"name": "collectNativeNodes"
},
Expand Down

0 comments on commit dd64c6c

Please sign in to comment.