Skip to content

Commit d1bfb29

Browse files
committed
Mark trees that need propagation in readContext
Instead of storing matched context consumers in a Set, we can mark when a consumer receives an update inside `readContext`. I hesistated to put anything in this function because it's such a hot path, but so are bail outs. Fortunately, we only need to set this flag once, the first time a context is read. So I think it's a reasonable trade off. In exchange, propagation is faster because we no longer need to accumulate a Set of matched consumers, and fiber bailouts are faster because we don't need to consult that Set. And the code is simpler.
1 parent 8ff5593 commit d1bfb29

5 files changed

+100
-185
lines changed

packages/react-reconciler/src/ReactFiberFlags.js

+24-23
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,51 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
1212
export type Flags = number;
1313

1414
// Don't change these two values. They're used by React Dev Tools.
15-
export const NoFlags = /* */ 0b000000000000000000000;
16-
export const PerformedWork = /* */ 0b000000000000000000001;
15+
export const NoFlags = /* */ 0b0000000000000000000000;
16+
export const PerformedWork = /* */ 0b0000000000000000000001;
1717

1818
// You can change the rest (and add more).
19-
export const Placement = /* */ 0b000000000000000000010;
20-
export const Update = /* */ 0b000000000000000000100;
19+
export const Placement = /* */ 0b0000000000000000000010;
20+
export const Update = /* */ 0b0000000000000000000100;
2121
export const PlacementAndUpdate = /* */ Placement | Update;
22-
export const Deletion = /* */ 0b000000000000000001000;
23-
export const ChildDeletion = /* */ 0b000000000000000010000;
24-
export const ContentReset = /* */ 0b000000000000000100000;
25-
export const Callback = /* */ 0b000000000000001000000;
26-
export const DidCapture = /* */ 0b000000000000010000000;
27-
export const Ref = /* */ 0b000000000000100000000;
28-
export const Snapshot = /* */ 0b000000000001000000000;
29-
export const Passive = /* */ 0b000000000010000000000;
30-
export const Hydrating = /* */ 0b000000000100000000000;
22+
export const Deletion = /* */ 0b0000000000000000001000;
23+
export const ChildDeletion = /* */ 0b0000000000000000010000;
24+
export const ContentReset = /* */ 0b0000000000000000100000;
25+
export const Callback = /* */ 0b0000000000000001000000;
26+
export const DidCapture = /* */ 0b0000000000000010000000;
27+
export const Ref = /* */ 0b0000000000000100000000;
28+
export const Snapshot = /* */ 0b0000000000001000000000;
29+
export const Passive = /* */ 0b0000000000010000000000;
30+
export const Hydrating = /* */ 0b0000000000100000000000;
3131
export const HydratingAndUpdate = /* */ Hydrating | Update;
32-
export const Visibility = /* */ 0b000000001000000000000;
32+
export const Visibility = /* */ 0b0000000001000000000000;
3333

3434
export const LifecycleEffectMask = Passive | Update | Callback | Ref | Snapshot;
3535

3636
// Union of all commit flags (flags with the lifetime of a particular commit)
37-
export const HostEffectMask = /* */ 0b000000001111111111111;
37+
export const HostEffectMask = /* */ 0b0000000001111111111111;
3838

3939
// These are not really side effects, but we still reuse this field.
40-
export const Incomplete = /* */ 0b000000010000000000000;
41-
export const ShouldCapture = /* */ 0b000000100000000000000;
40+
export const Incomplete = /* */ 0b0000000010000000000000;
41+
export const ShouldCapture = /* */ 0b0000000100000000000000;
4242
// TODO (effects) Remove this bit once the new reconciler is synced to the old.
43-
export const PassiveUnmountPendingDev = /* */ 0b000001000000000000000;
44-
export const ForceUpdateForLegacySuspense = /* */ 0b000010000000000000000;
45-
export const DidPropagateContext = /* */ 0b000100000000000000000;
43+
export const PassiveUnmountPendingDev = /* */ 0b0000001000000000000000;
44+
export const ForceUpdateForLegacySuspense = /* */ 0b0000010000000000000000;
45+
export const DidPropagateContext = /* */ 0b0000100000000000000000;
46+
export const NeedsPropagation = /* */ 0b0001000000000000000000;
4647

4748
// Static tags describe aspects of a fiber that are not specific to a render,
4849
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
4950
// This enables us to defer more work in the unmount case,
5051
// since we can defer traversing the tree during layout to look for Passive effects,
5152
// and instead rely on the static flag as a signal that there may be cleanup work.
52-
export const PassiveStatic = /* */ 0b001000000000000000000;
53+
export const PassiveStatic = /* */ 0b0010000000000000000000;
5354

5455
// These flags allow us to traverse to fibers that have effects on mount
5556
// without traversing the entire tree after every commit for
5657
// double invoking
57-
export const MountLayoutDev = /* */ 0b010000000000000000000;
58-
export const MountPassiveDev = /* */ 0b100000000000000000000;
58+
export const MountLayoutDev = /* */ 0b0100000000000000000000;
59+
export const MountPassiveDev = /* */ 0b1000000000000000000000;
5960

6061
// Groups of flags that are used in the commit phase to skip over trees that
6162
// don't contain effects, by checking subtreeFlags.

packages/react-reconciler/src/ReactFiberNewContext.new.js

+37-75
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {
3333
mergeLanes,
3434
pickArbitraryLane,
3535
} from './ReactFiberLane.new';
36-
import {NoFlags, DidPropagateContext} from './ReactFiberFlags';
36+
import {
37+
NoFlags,
38+
DidPropagateContext,
39+
NeedsPropagation,
40+
} from './ReactFiberFlags';
3741

3842
import invariant from 'shared/invariant';
3943
import is from 'shared/objectIs';
@@ -409,14 +413,6 @@ function propagateContextChanges<T>(
409413
// visit them during render. We should continue propagating the
410414
// siblings, though
411415
nextFiber = null;
412-
413-
// Keep track of subtrees whose propagation we deferred
414-
if (deferredPropagation === null) {
415-
deferredPropagation = new Set([consumer]);
416-
} else {
417-
deferredPropagation.add(consumer);
418-
}
419-
nextFiber = null;
420416
}
421417

422418
// Since we already found a match, we can stop traversing the
@@ -513,29 +509,14 @@ export function propagateParentContextChangesToDeferredTree(
513509
);
514510
}
515511

516-
// Used by lazy context propagation algorithm. When we find a context dependency
517-
// match, we don't propagate the changes any further into that fiber's subtree.
518-
// We add the matched fibers to this set. Later, if something inside that
519-
// subtree bails out of rendering, the presence of a parent fiber in this Set
520-
// tells us that we need to continue propagating.
521-
//
522-
// This is a set of _current_ fibers, not work-in-progress fibers. That's why
523-
// it's a set instead of a flag on the fiber.
524-
let deferredPropagation: Set<Fiber> | null = null;
525-
526-
export function resetDeferredContextPropagation() {
527-
// This is called by prepareFreshStack
528-
deferredPropagation = null;
529-
}
530-
531512
function propagateParentContextChanges(
532513
current: Fiber,
533514
workInProgress: Fiber,
534515
renderLanes: Lanes,
535516
forcePropagateEntireTree: boolean,
536517
) {
537518
if (!enableLazyContextPropagation) {
538-
return false;
519+
return;
539520
}
540521

541522
// Collect all the parent providers that changed. Since this is usually small
@@ -544,58 +525,36 @@ function propagateParentContextChanges(
544525
let parent = workInProgress;
545526
let isInsidePropagationBailout = false;
546527
while (parent !== null) {
547-
const currentParent = parent.alternate;
548-
invariant(
549-
currentParent !== null,
550-
'Should have a current fiber. This is a bug in React.',
551-
);
552-
553528
if (!isInsidePropagationBailout) {
554-
if (deferredPropagation === null) {
555-
if ((parent.flags & DidPropagateContext) !== NoFlags) {
556-
break;
557-
}
558-
} else {
559-
if (currentParent !== null && deferredPropagation.has(currentParent)) {
560-
// We're inside a subtree that previously bailed out of propagation.
561-
// We must disregard the the DidPropagateContext flag as we continue
562-
// searching for parent providers.
563-
isInsidePropagationBailout = true;
564-
// We know that none of the providers in between the propagation
565-
// bailout and the nearest render bailout above that could have
566-
// changed. So we can skip those.
567-
do {
568-
parent = parent.return;
569-
invariant(
570-
parent !== null,
571-
'Expected to find a bailed out fiber. This is a bug in React.',
572-
);
573-
} while ((parent.flags & DidPropagateContext) === NoFlags);
574-
} else if ((parent.flags & DidPropagateContext) !== NoFlags) {
575-
break;
576-
}
529+
if ((parent.flags & NeedsPropagation) !== NoFlags) {
530+
isInsidePropagationBailout = true;
531+
} else if ((parent.flags & DidPropagateContext) !== NoFlags) {
532+
break;
577533
}
578534
}
579535

580536
if (parent.tag === ContextProvider) {
581-
if (currentParent !== null) {
582-
const oldProps = currentParent.memoizedProps;
583-
if (oldProps !== null) {
584-
const providerType: ReactProviderType<any> = parent.type;
585-
const context: ReactContext<any> = providerType._context;
586-
587-
const newProps = parent.pendingProps;
588-
const newValue = newProps.value;
589-
590-
const oldValue = oldProps.value;
591-
592-
const changedBits = calculateChangedBits(context, newValue, oldValue);
593-
if (changedBits !== 0) {
594-
if (contexts !== null) {
595-
contexts.push(context, changedBits);
596-
} else {
597-
contexts = [context, changedBits];
598-
}
537+
const currentParent = parent.alternate;
538+
invariant(
539+
currentParent !== null,
540+
'Should have a current fiber. This is a bug in React.',
541+
);
542+
const oldProps = currentParent.memoizedProps;
543+
if (oldProps !== null) {
544+
const providerType: ReactProviderType<any> = parent.type;
545+
const context: ReactContext<any> = providerType._context;
546+
547+
const newProps = parent.pendingProps;
548+
const newValue = newProps.value;
549+
550+
const oldValue = oldProps.value;
551+
552+
const changedBits = calculateChangedBits(context, newValue, oldValue);
553+
if (changedBits !== 0) {
554+
if (contexts !== null) {
555+
contexts.push(context, changedBits);
556+
} else {
557+
contexts = [context, changedBits];
599558
}
600559
}
601560
}
@@ -628,10 +587,10 @@ function propagateParentContextChanges(
628587
//
629588
// Unfortunately, though, we need to ignore this flag when we're inside a
630589
// tree whose context propagation was deferred — that's what the
631-
// `deferredPropagation` set is for.
590+
// `NeedsPropagation` flag is for.
632591
//
633-
// If we could instead bail out before entering the siblings' beging phase,
634-
// then we could remove both `DidPropagateContext` and `deferredPropagation`.
592+
// If we could instead bail out before entering the siblings' begin phase,
593+
// then we could remove both `DidPropagateContext` and `NeedsPropagation`.
635594
// Consider this as part of the next refactor to the fiber tree structure.
636595
workInProgress.flags |= DidPropagateContext;
637596
}
@@ -750,6 +709,9 @@ export function readContext<T>(
750709
// TODO: This is an old field. Delete it.
751710
responders: null,
752711
};
712+
if (enableLazyContextPropagation) {
713+
currentlyRenderingFiber.flags |= NeedsPropagation;
714+
}
753715
} else {
754716
// Append a new context item.
755717
lastContextDependency = lastContextDependency.next = contextItem;

0 commit comments

Comments
 (0)