Don't stop context traversal at matching consumers (#13391)
* Don't stop context traversal at matching consumers Originally, the idea was to time slice the traversal. This worked when there was only a single context type per consumer. Now that each fiber may have a list of context dependencies, including duplicate entries, that optimization no longer makes sense – we could end up scanning the same subtree multiple times. * Remove changedBits from context object and stack Don't need it anymore, yay
Andrew Clark committed
Aug 15, 2018 at 11:19 UTC
1bc975d073dd6f60217d645b43a84e79f96a216b
5 files changed
+35
-84
packages/react-reconciler/src/ReactFiberBeginWork.js
+9
-18
@@ -96,7 +96,6 @@ import {
96
resumeMountClassInstance,
97
updateClassInstance,
98
} from './ReactFiberClassComponent';
99
-import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
99
100
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
101
@@ -290,10 +289,7 @@ function updateClassComponent(
289
// During mounting we don't know the child context yet as the instance doesn't exist.
290
// We will invalidate the child context in finishClassComponent() right after rendering.
291
const hasContext = pushLegacyContextProvider(workInProgress);
293
- const hasPendingNewContext = prepareToReadContext(
294
- workInProgress,
295
- renderExpirationTime,
296
- );
292
+ prepareToReadContext(workInProgress, renderExpirationTime);
293
294
let shouldUpdate;
295
if (current === null) {
@@ -311,7 +307,6 @@ function updateClassComponent(
307
// In a resume, we'll already have an instance we can reuse.
308
shouldUpdate = resumeMountClassInstance(
309
workInProgress,
314
- hasPendingNewContext,
310
renderExpirationTime,
311
);
312
}
@@ -319,7 +314,6 @@ function updateClassComponent(
314
shouldUpdate = updateClassInstance(
315
current,
316
workInProgress,
322
- hasPendingNewContext,
317
renderExpirationTime,
318
);
319
}
@@ -841,20 +835,17 @@ function updateContextProvider(
835
}
836
}
837
844
- let changedBits: number;
845
- if (oldProps === null) {
846
- // Initial render
847
- changedBits = MAX_SIGNED_31_BIT_INT;
848
- } else {
838
+ pushProvider(workInProgress, newValue);
839
+
840
+ if (oldProps !== null) {
841
const oldValue = oldProps.value;
850
- changedBits = calculateChangedBits(context, newValue, oldValue);
842
+ const changedBits = calculateChangedBits(context, newValue, oldValue);
843
if (changedBits === 0) {
844
// No change. Bailout early if children are the same.
845
if (
846
oldProps.children === newProps.children &&
847
!hasLegacyContextChanged()
848
) {
857
- pushProvider(workInProgress, 0);
849
return bailoutOnAlreadyFinishedWork(
850
current,
851
workInProgress,
@@ -873,8 +864,6 @@ function updateContextProvider(
864
}
865
}
866
876
- pushProvider(workInProgress, changedBits);
877
-
867
const newChildren = newProps.children;
868
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
869
return workInProgress.child;
@@ -1014,9 +1003,11 @@ function beginWork(
1003
workInProgress.stateNode.containerInfo,
1004
);
1005
break;
1017
- case ContextProvider:
1018
- pushProvider(workInProgress, 0);
1006
+ case ContextProvider: {
1007
+ const newValue = workInProgress.memoizedProps.value;
1008
+ pushProvider(workInProgress, newValue);
1009
break;
1010
+ }
1011
case Profiler:
1012
if (enableProfilerTimer) {
1013
workInProgress.effectTag |= Update;
packages/react-reconciler/src/ReactFiberClassComponent.js
-6
@@ -744,7 +744,6 @@ function mountClassInstance(
744
745
function resumeMountClassInstance(
746
workInProgress: Fiber,
747
- hasPendingNewContext: boolean,
747
renderExpirationTime: ExpirationTime,
748
): boolean {
749
const ctor = workInProgress.type;
@@ -806,7 +805,6 @@ function resumeMountClassInstance(
805
oldProps === newProps &&
806
oldState === newState &&
807
!hasContextChanged() &&
809
- !hasPendingNewContext &&
808
!checkHasForceUpdateAfterProcessing()
809
) {
810
// If an update was already in progress, we should schedule an Update
@@ -828,7 +826,6 @@ function resumeMountClassInstance(
826
827
const shouldUpdate =
828
checkHasForceUpdateAfterProcessing() ||
831
- hasPendingNewContext ||
829
checkShouldComponentUpdate(
830
workInProgress,
831
oldProps,
@@ -884,7 +881,6 @@ function resumeMountClassInstance(
881
function updateClassInstance(
882
current: Fiber,
883
workInProgress: Fiber,
887
- hasPendingNewContext: boolean,
884
renderExpirationTime: ExpirationTime,
885
): boolean {
886
const ctor = workInProgress.type;
@@ -947,7 +943,6 @@ function updateClassInstance(
943
oldProps === newProps &&
944
oldState === newState &&
945
!hasContextChanged() &&
950
- !hasPendingNewContext &&
946
!checkHasForceUpdateAfterProcessing()
947
) {
948
// If an update was already in progress, we should schedule an Update
@@ -982,7 +977,6 @@ function updateClassInstance(
977
978
const shouldUpdate =
979
checkHasForceUpdateAfterProcessing() ||
985
- hasPendingNewContext ||
980
checkShouldComponentUpdate(
981
workInProgress,
982
oldProps,
packages/react-reconciler/src/ReactFiberNewContext.js
+26
-56
@@ -23,14 +23,18 @@ import {isPrimaryRenderer} from './ReactFiberHostConfig';
23
import {createCursor, push, pop} from './ReactFiberStack';
24
import maxSigned31BitInt from './maxSigned31BitInt';
25
import {NoWork} from './ReactFiberExpirationTime';
26
-import {ContextProvider} from 'shared/ReactTypeOfWork';
26
+import {ContextProvider, ClassComponent} from 'shared/ReactTypeOfWork';
27
28
import invariant from 'shared/invariant';
29
import warning from 'shared/warning';
30
+import {
31
+ createUpdate,
32
+ enqueueUpdate,
33
+ ForceUpdate,
34
+} from 'react-reconciler/src/ReactUpdateQueue';
35
36
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
37
const valueCursor: StackCursor<mixed> = createCursor(null);
33
-const changedBitsCursor: StackCursor<number> = createCursor(0);
38
39
let rendererSigil;
40
if (__DEV__) {
@@ -50,15 +54,13 @@ export function resetContextDependences(): void {
54
lastContextWithAllBitsObserved = null;
55
}
56
53
-export function pushProvider(providerFiber: Fiber, changedBits: number): void {
54
- const context: ReactContext<any> = providerFiber.type._context;
57
+export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
58
+ const context: ReactContext<T> = providerFiber.type._context;
59
60
if (isPrimaryRenderer) {
57
- push(changedBitsCursor, context._changedBits, providerFiber);
61
push(valueCursor, context._currentValue, providerFiber);
62
60
- context._currentValue = providerFiber.pendingProps.value;
61
- context._changedBits = changedBits;
63
+ context._currentValue = nextValue;
64
if (__DEV__) {
65
warningWithoutStack(
66
context._currentRenderer === undefined ||
@@ -70,11 +72,9 @@ export function pushProvider(providerFiber: Fiber, changedBits: number): void {
72
context._currentRenderer = rendererSigil;
73
}
74
} else {
73
- push(changedBitsCursor, context._changedBits2, providerFiber);
75
push(valueCursor, context._currentValue2, providerFiber);
76
76
- context._currentValue2 = providerFiber.pendingProps.value;
77
- context._changedBits2 = changedBits;
77
+ context._currentValue2 = nextValue;
78
if (__DEV__) {
79
warningWithoutStack(
80
context._currentRenderer2 === undefined ||
@@ -89,19 +89,15 @@ export function pushProvider(providerFiber: Fiber, changedBits: number): void {
89
}
90
91
export function popProvider(providerFiber: Fiber): void {
92
- const changedBits = changedBitsCursor.current;
92
const currentValue = valueCursor.current;
93
94
pop(valueCursor, providerFiber);
96
- pop(changedBitsCursor, providerFiber);
95
96
const context: ReactContext<any> = providerFiber.type._context;
97
if (isPrimaryRenderer) {
98
context._currentValue = currentValue;
101
- context._changedBits = changedBits;
99
} else {
100
context._currentValue2 = currentValue;
104
- context._changedBits2 = changedBits;
101
}
102
}
103
@@ -162,6 +158,18 @@ export function propagateContextChange(
158
(dependency.observedBits & changedBits) !== 0
159
) {
160
// Match! Schedule an update on this fiber.
161
+
162
+ if (fiber.tag === ClassComponent) {
163
+ // Schedule a force update on the work-in-progress.
164
+ const update = createUpdate(renderExpirationTime);
165
+ update.tag = ForceUpdate;
166
+ // TODO: Because we don't have a work-in-progress, this will add the
167
+ // update to the current fiber, too, which means it will persist even if
168
+ // this render is thrown away. Since it's a race condition, not sure it's
169
+ // worth fixing.
170
+ enqueueUpdate(fiber, update);
171
+ }
172
+
173
if (
174
fiber.expirationTime === NoWork ||
175
fiber.expirationTime > renderExpirationTime
@@ -206,13 +214,8 @@ export function propagateContextChange(
214
}
215
node = node.return;
216
}
209
- // Don't scan deeper than a matching consumer. When we render the
210
- // consumer, we'll continue scanning from that point. This way the
211
- // scanning work is time-sliced.
212
- nextFiber = null;
213
- } else {
214
- nextFiber = fiber.child;
217
}
218
+ nextFiber = fiber.child;
219
dependency = dependency.next;
220
} while (dependency !== null);
221
} else if (fiber.tag === ContextProvider) {
@@ -253,46 +256,13 @@ export function propagateContextChange(
256
export function prepareToReadContext(
257
workInProgress: Fiber,
258
renderExpirationTime: ExpirationTime,
256
-): boolean {
259
+): void {
260
currentlyRenderingFiber = workInProgress;
261
lastContextDependency = null;
262
lastContextWithAllBitsObserved = null;
263
261
- const firstContextDependency = workInProgress.firstContextDependency;
262
- if (firstContextDependency !== null) {
263
- // Reset the work-in-progress list
264
- workInProgress.firstContextDependency = null;
265
-
266
- // Iterate through the context dependencies and see if there were any
267
- // changes. If so, continue propagating the context change by scanning
268
- // the child subtree.
269
- let dependency = firstContextDependency;
270
- let hasPendingContext = false;
271
- do {
272
- const context = dependency.context;
273
- const changedBits = isPrimaryRenderer
274
- ? context._changedBits
275
- : context._changedBits2;
276
- if (changedBits !== 0) {
277
- // Resume context change propagation. We need to call this even if
278
- // this fiber bails out, in case deeply nested consumers observe more
279
- // bits than this one.
280
- propagateContextChange(
281
- workInProgress,
282
- context,
283
- changedBits,
284
- renderExpirationTime,
285
- );
286
- if ((changedBits & dependency.observedBits) !== 0) {
287
- hasPendingContext = true;
288
- }
289
- }
290
- dependency = dependency.next;
291
- } while (dependency !== null);
292
- return hasPendingContext;
293
- } else {
294
- return false;
295
- }
264
+ // Reset the work-in-progress list
265
+ workInProgress.firstContextDependency = null;
266
}
267
268
export function readContext<T>(
packages/react/src/ReactContext.js
-2
@@ -57,8 +57,6 @@ export function createContext<T>(
57
// Secondary renderers store their context values on separate fields.
58
_currentValue: defaultValue,
59
_currentValue2: defaultValue,
60
- _changedBits: 0,
61
- _changedBits2: 0,
60
// These are circular
61
Provider: (null: any),
62
Consumer: (null: any),
packages/shared/ReactTypes.js
-2
@@ -85,8 +85,6 @@ export type ReactContext<T> = {
85
86
_currentValue: T,
87
_currentValue2: T,
88
- _changedBits: number,
89
- _changedBits2: number,
88
89
// DEV only
90
_currentRenderer?: Object | null,