Land interleaved updates change in main fork (#20710)
* Land #20615 in main fork Includes change to interleaved updates. ``` yarn replace-fork ``` * Check deferRenderPhaseUpdateToNextBatch in test
Andrew Clark committed
Feb 1, 2021 at 18:05 UTC
7cb9fd7ef822436aef13c8cbf648af1e21a5309a
9 files changed
+219
-40
packages/react-reconciler/src/ReactFiberClassComponent.old.js
+3
-3
@@ -214,7 +214,7 @@ const classComponentUpdater = {
214
update.callback = callback;
215
}
216
217
- enqueueUpdate(fiber, update);
217
+ enqueueUpdate(fiber, update, lane);
218
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
219
if (root !== null) {
220
entangleTransitions(root, fiber, lane);
@@ -249,7 +249,7 @@ const classComponentUpdater = {
249
update.callback = callback;
250
}
251
252
- enqueueUpdate(fiber, update);
252
+ enqueueUpdate(fiber, update, lane);
253
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
254
if (root !== null) {
255
entangleTransitions(root, fiber, lane);
@@ -283,7 +283,7 @@ const classComponentUpdater = {
283
update.callback = callback;
284
}
285
286
- enqueueUpdate(fiber, update);
286
+ enqueueUpdate(fiber, update, lane);
287
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
288
if (root !== null) {
289
entangleTransitions(root, fiber, lane);
packages/react-reconciler/src/ReactFiberHooks.old.js
+58
-14
@@ -77,6 +77,7 @@ import {
77
warnIfNotCurrentlyActingUpdatesInDev,
78
warnIfNotScopedWithMatchingAct,
79
markSkippedUpdateLanes,
80
+ isInterleavedUpdate,
81
} from './ReactFiberWorkLoop.old';
82
83
import invariant from 'shared/invariant';
@@ -110,6 +111,7 @@ import {
111
enqueueUpdate,
112
entangleTransitions,
113
} from './ReactUpdateQueue.old';
114
+import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
115
116
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
117
@@ -122,8 +124,9 @@ type Update<S, A> = {|
124
priority?: ReactPriorityLevel,
125
|};
126
125
-type UpdateQueue<S, A> = {|
127
+export type UpdateQueue<S, A> = {|
128
pending: Update<S, A> | null,
129
+ interleaved: Update<S, A> | null,
130
lanes: Lanes,
131
dispatch: (A => mixed) | null,
132
lastRenderedReducer: ((S, A) => S) | null,
@@ -657,6 +660,7 @@ function mountReducer<S, I, A>(
660
hook.memoizedState = hook.baseState = initialState;
661
const queue = (hook.queue = {
662
pending: null,
663
+ interleaved: null,
664
lanes: NoLanes,
665
dispatch: null,
666
lastRenderedReducer: reducer,
@@ -800,7 +804,22 @@ function updateReducer<S, I, A>(
804
queue.lastRenderedState = newState;
805
}
806
803
- if (baseQueue === null) {
807
+ // Interleaved updates are stored on a separate queue. We aren't going to
808
+ // process them during this render, but we do need to track which lanes
809
+ // are remaining.
810
+ const lastInterleaved = queue.interleaved;
811
+ if (lastInterleaved !== null) {
812
+ let interleaved = lastInterleaved;
813
+ do {
814
+ const interleavedLane = interleaved.lane;
815
+ currentlyRenderingFiber.lanes = mergeLanes(
816
+ currentlyRenderingFiber.lanes,
817
+ interleavedLane,
818
+ );
819
+ markSkippedUpdateLanes(interleavedLane);
820
+ interleaved = ((interleaved: any).next: Update<S, A>);
821
+ } while (interleaved !== lastInterleaved);
822
+ } else if (baseQueue === null) {
823
// `queue.lanes` is used for entangling transitions. We can set it back to
824
// zero once the queue is empty.
825
queue.lanes = NoLanes;
@@ -1132,6 +1151,7 @@ function useMutableSource<Source, Snapshot>(
1151
// including any interleaving updates that occur.
1152
const newQueue = {
1153
pending: null,
1154
+ interleaved: null,
1155
lanes: NoLanes,
1156
dispatch: null,
1157
lastRenderedReducer: basicStateReducer,
@@ -1188,6 +1208,7 @@ function mountState<S>(
1208
hook.memoizedState = hook.baseState = initialState;
1209
const queue = (hook.queue = {
1210
pending: null,
1211
+ interleaved: null,
1212
lanes: NoLanes,
1213
dispatch: null,
1214
lastRenderedReducer: basicStateReducer,
@@ -1869,7 +1890,7 @@ function refreshCache<T>(fiber: Fiber, seedKey: ?() => T, seedValue: T) {
1890
cache: seededCache,
1891
};
1892
refreshUpdate.payload = payload;
1872
- enqueueUpdate(provider, refreshUpdate);
1893
+ enqueueUpdate(provider, refreshUpdate, lane);
1894
return;
1895
}
1896
}
@@ -1904,17 +1925,6 @@ function dispatchAction<S, A>(
1925
next: (null: any),
1926
};
1927
1907
- // Append the update to the end of the list.
1908
- const pending = queue.pending;
1909
- if (pending === null) {
1910
- // This is the first update. Create a circular list.
1911
- update.next = update;
1912
- } else {
1913
- update.next = pending.next;
1914
- pending.next = update;
1915
- }
1916
- queue.pending = update;
1917
-
1928
const alternate = fiber.alternate;
1929
if (
1930
fiber === currentlyRenderingFiber ||
@@ -1924,7 +1934,41 @@ function dispatchAction<S, A>(
1934
// queue -> linked list of updates. After this render pass, we'll restart
1935
// and apply the stashed updates on top of the work-in-progress hook.
1936
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
1937
+ const pending = queue.pending;
1938
+ if (pending === null) {
1939
+ // This is the first update. Create a circular list.
1940
+ update.next = update;
1941
+ } else {
1942
+ update.next = pending.next;
1943
+ pending.next = update;
1944
+ }
1945
+ queue.pending = update;
1946
} else {
1947
+ if (isInterleavedUpdate(fiber, lane)) {
1948
+ const interleaved = queue.interleaved;
1949
+ if (interleaved === null) {
1950
+ // This is the first update. Create a circular list.
1951
+ update.next = update;
1952
+ // At the end of the current render, this queue's interleaved updates will
1953
+ // be transfered to the pending queue.
1954
+ pushInterleavedQueue(queue);
1955
+ } else {
1956
+ update.next = interleaved.next;
1957
+ interleaved.next = update;
1958
+ }
1959
+ queue.interleaved = update;
1960
+ } else {
1961
+ const pending = queue.pending;
1962
+ if (pending === null) {
1963
+ // This is the first update. Create a circular list.
1964
+ update.next = update;
1965
+ } else {
1966
+ update.next = pending.next;
1967
+ pending.next = update;
1968
+ }
1969
+ queue.pending = update;
1970
+ }
1971
+
1972
if (
1973
fiber.lanes === NoLanes &&
1974
(alternate === null || alternate.lanes === NoLanes)
packages/react-reconciler/src/ReactFiberInterleavedUpdates.old.js
new
+55
@@ -0,0 +1,55 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {UpdateQueue as HookQueue} from './ReactFiberHooks.old';
11
+import type {SharedQueue as ClassQueue} from './ReactUpdateQueue.old';
12
+
13
+// An array of all update queues that received updates during the current
14
+// render. When this render exits, either because it finishes or because it is
15
+// interrupted, the interleaved updates will be transfered onto the main part
16
+// of the queue.
17
+let interleavedQueues: Array<
18
+ HookQueue<any, any> | ClassQueue<any>,
19
+> | null = null;
20
+
21
+export function pushInterleavedQueue(
22
+ queue: HookQueue<any, any> | ClassQueue<any>,
23
+) {
24
+ if (interleavedQueues === null) {
25
+ interleavedQueues = [queue];
26
+ } else {
27
+ interleavedQueues.push(queue);
28
+ }
29
+}
30
+
31
+export function enqueueInterleavedUpdates() {
32
+ // Transfer the interleaved updates onto the main queue. Each queue has a
33
+ // `pending` field and an `interleaved` field. When they are not null, they
34
+ // point to the last node in a circular linked list. We need to append the
35
+ // interleaved list to the end of the pending list by joining them into a
36
+ // single, circular list.
37
+ if (interleavedQueues !== null) {
38
+ for (let i = 0; i < interleavedQueues.length; i++) {
39
+ const queue = interleavedQueues[i];
40
+ const lastInterleavedUpdate = queue.interleaved;
41
+ if (lastInterleavedUpdate !== null) {
42
+ queue.interleaved = null;
43
+ const firstInterleavedUpdate = lastInterleavedUpdate.next;
44
+ const lastPendingUpdate = queue.pending;
45
+ if (lastPendingUpdate !== null) {
46
+ const firstPendingUpdate = lastPendingUpdate.next;
47
+ lastPendingUpdate.next = (firstInterleavedUpdate: any);
48
+ lastInterleavedUpdate.next = (firstPendingUpdate: any);
49
+ }
50
+ queue.pending = (lastInterleavedUpdate: any);
51
+ }
52
+ }
53
+ interleavedQueues = null;
54
+ }
55
+}
packages/react-reconciler/src/ReactFiberNewContext.old.js
+21
-6
@@ -11,6 +11,7 @@ import type {ReactContext} from 'shared/ReactTypes';
11
import type {Fiber, ContextDependency} from './ReactInternalTypes';
12
import type {StackCursor} from './ReactFiberStack.old';
13
import type {Lanes} from './ReactFiberLane.old';
14
+import type {SharedQueue} from './ReactUpdateQueue.old';
15
16
import {isPrimaryRenderer} from './ReactFiberHostConfig';
17
import {createCursor, push, pop} from './ReactFiberStack.old';
@@ -31,7 +32,7 @@ import {
32
33
import invariant from 'shared/invariant';
34
import is from 'shared/objectIs';
34
-import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.old';
35
+import {createUpdate, ForceUpdate} from './ReactUpdateQueue.old';
36
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.old';
37
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
38
@@ -211,16 +212,30 @@ export function propagateContextChange<T>(
212
213
if (fiber.tag === ClassComponent) {
214
// Schedule a force update on the work-in-progress.
214
- const update = createUpdate(
215
- NoTimestamp,
216
- pickArbitraryLane(renderLanes),
217
- );
215
+ const lane = pickArbitraryLane(renderLanes);
216
+ const update = createUpdate(NoTimestamp, lane);
217
update.tag = ForceUpdate;
218
// TODO: Because we don't have a work-in-progress, this will add the
219
// update to the current fiber, too, which means it will persist even if
220
// this render is thrown away. Since it's a race condition, not sure it's
221
// worth fixing.
223
- enqueueUpdate(fiber, update);
222
+
223
+ // Inlined `enqueueUpdate` to remove interleaved update check
224
+ const updateQueue = fiber.updateQueue;
225
+ if (updateQueue === null) {
226
+ // Only occurs if the fiber has been unmounted.
227
+ } else {
228
+ const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
229
+ const pending = sharedQueue.pending;
230
+ if (pending === null) {
231
+ // This is the first update. Create a circular list.
232
+ update.next = update;
233
+ } else {
234
+ update.next = pending.next;
235
+ pending.next = update;
236
+ }
237
+ sharedQueue.pending = update;
238
+ }
239
}
240
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
241
const alternate = fiber.alternate;
packages/react-reconciler/src/ReactFiberReconciler.old.js
+1
-1
@@ -318,7 +318,7 @@ export function updateContainer(
318
update.callback = callback;
319
}
320
321
- enqueueUpdate(current, update);
321
+ enqueueUpdate(current, update, lane);
322
const root = scheduleUpdateOnFiber(current, lane, eventTime);
323
if (root !== null) {
324
entangleTransitions(root, current, lane);
packages/react-reconciler/src/ReactFiberThrow.old.js
+1
-1
@@ -293,7 +293,7 @@ function throwException(
293
// prevent a bail out.
294
const update = createUpdate(NoTimestamp, SyncLane);
295
update.tag = ForceUpdate;
296
- enqueueUpdate(sourceFiber, update);
296
+ enqueueUpdate(sourceFiber, update, SyncLane);
297
}
298
}
299
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+23
-3
@@ -34,7 +34,6 @@ import {
34
disableSchedulerTimeoutInWorkLoop,
35
enableDoubleInvokingEffects,
36
skipUnmountedBoundaries,
37
- enableDiscreteEventMicroTasks,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
39
import invariant from 'shared/invariant';
@@ -207,6 +206,7 @@ import {
206
pop as popFromStack,
207
createCursor,
208
} from './ReactFiberStack.old';
209
+import {enqueueInterleavedUpdates} from './ReactFiberInterleavedUpdates.old';
210
211
import {
212
markNestedUpdateScheduled,
@@ -217,6 +217,7 @@ import {
217
syncNestedUpdateFlag,
218
} from './ReactProfilerTimer.old';
219
220
+import {enableDiscreteEventMicroTasks} from 'shared/ReactFeatureFlags';
221
// DEV stuff
222
import getComponentName from 'shared/getComponentName';
223
import ReactStrictModeWarnings from './ReactStrictModeWarnings.old';
@@ -537,6 +538,7 @@ export function scheduleUpdateOnFiber(
538
}
539
}
540
541
+ // TODO: Consolidate with `isInterleavedUpdate` check
542
if (root === workInProgressRoot) {
543
// Received an update to a tree that's in the middle of rendering. Mark
544
// that there was an interleaved update work on this root. Unless the
@@ -674,6 +676,22 @@ function markUpdateLaneFromFiberToRoot(
676
}
677
}
678
679
+export function isInterleavedUpdate(fiber: Fiber, lane: Lane) {
680
+ return (
681
+ // TODO: Optimize slightly by comparing to root that fiber belongs to.
682
+ // Requires some refactoring. Not a big deal though since it's rare for
683
+ // concurrent apps to have more than a single root.
684
+ workInProgressRoot !== null &&
685
+ (fiber.mode & BlockingMode) !== NoMode &&
686
+ // If this is a render phase update (i.e. UNSAFE_componentWillReceiveProps),
687
+ // then don't treat this as an interleaved update. This pattern is
688
+ // accompanied by a warning but we haven't fully deprecated it yet. We can
689
+ // remove once the deferRenderPhaseUpdateToNextBatch flag is enabled.
690
+ (deferRenderPhaseUpdateToNextBatch ||
691
+ (executionContext & RenderContext) === NoContext)
692
+ );
693
+}
694
+
695
// Use this function to schedule a task for a root. There's only one task per
696
// root; if a task was already scheduled, we'll check to make sure the priority
697
// of the existing task is the same as the priority of the next level that the
@@ -1376,6 +1394,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1394
workInProgressRootUpdatedLanes = NoLanes;
1395
workInProgressRootPingedLanes = NoLanes;
1396
1397
+ enqueueInterleavedUpdates();
1398
+
1399
if (enableSchedulerTracing) {
1400
spawnedWorkDuringRender = null;
1401
}
@@ -2307,7 +2327,7 @@ function captureCommitPhaseErrorOnRoot(
2327
) {
2328
const errorInfo = createCapturedValue(error, sourceFiber);
2329
const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2310
- enqueueUpdate(rootFiber, update);
2330
+ enqueueUpdate(rootFiber, update, (SyncLane: Lane));
2331
const eventTime = requestEventTime();
2332
const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
2333
if (root !== null) {
@@ -2354,7 +2374,7 @@ export function captureCommitPhaseError(
2374
errorInfo,
2375
(SyncLane: Lane),
2376
);
2357
- enqueueUpdate(fiber, update);
2377
+ enqueueUpdate(fiber, update, (SyncLane: Lane));
2378
const eventTime = requestEventTime();
2379
const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
2380
if (root !== null) {
packages/react-reconciler/src/ReactUpdateQueue.old.js
+47
-11
@@ -105,7 +105,11 @@ import {Callback, ShouldCapture, DidCapture} from './ReactFiberFlags';
105
import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
106
107
import {StrictMode} from './ReactTypeOfMode';
108
-import {markSkippedUpdateLanes} from './ReactFiberWorkLoop.old';
108
+import {
109
+ markSkippedUpdateLanes,
110
+ isInterleavedUpdate,
111
+} from './ReactFiberWorkLoop.old';
112
+import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
113
114
import invariant from 'shared/invariant';
115
@@ -124,8 +128,9 @@ export type Update<State> = {|
128
next: Update<State> | null,
129
|};
130
127
-type SharedQueue<State> = {|
131
+export type SharedQueue<State> = {|
132
pending: Update<State> | null,
133
+ interleaved: Update<State> | null,
134
lanes: Lanes,
135
|};
136
@@ -165,6 +170,7 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
170
lastBaseUpdate: null,
171
shared: {
172
pending: null,
173
+ interleaved: null,
174
lanes: NoLanes,
175
},
176
effects: null,
@@ -205,7 +211,11 @@ export function createUpdate(eventTime: number, lane: Lane): Update<*> {
211
return update;
212
}
213
208
-export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
214
+export function enqueueUpdate<State>(
215
+ fiber: Fiber,
216
+ update: Update<State>,
217
+ lane: Lane,
218
+) {
219
const updateQueue = fiber.updateQueue;
220
if (updateQueue === null) {
221
// Only occurs if the fiber has been unmounted.
@@ -213,15 +223,31 @@ export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
223
}
224
225
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
216
- const pending = sharedQueue.pending;
217
- if (pending === null) {
218
- // This is the first update. Create a circular list.
219
- update.next = update;
226
+
227
+ if (isInterleavedUpdate(fiber, lane)) {
228
+ const interleaved = sharedQueue.interleaved;
229
+ if (interleaved === null) {
230
+ // This is the first update. Create a circular list.
231
+ update.next = update;
232
+ // At the end of the current render, this queue's interleaved updates will
233
+ // be transfered to the pending queue.
234
+ pushInterleavedQueue(sharedQueue);
235
+ } else {
236
+ update.next = interleaved.next;
237
+ interleaved.next = update;
238
+ }
239
+ sharedQueue.interleaved = update;
240
} else {
221
- update.next = pending.next;
222
- pending.next = update;
241
+ const pending = sharedQueue.pending;
242
+ if (pending === null) {
243
+ // This is the first update. Create a circular list.
244
+ update.next = update;
245
+ } else {
246
+ update.next = pending.next;
247
+ pending.next = update;
248
+ }
249
+ sharedQueue.pending = update;
250
}
224
- sharedQueue.pending = update;
251
252
if (__DEV__) {
253
if (
@@ -591,7 +617,17 @@ export function processUpdateQueue<State>(
617
queue.firstBaseUpdate = newFirstBaseUpdate;
618
queue.lastBaseUpdate = newLastBaseUpdate;
619
594
- if (firstBaseUpdate === null) {
620
+ // Interleaved updates are stored on a separate queue. We aren't going to
621
+ // process them during this render, but we do need to track which lanes
622
+ // are remaining.
623
+ const lastInterleaved = queue.shared.interleaved;
624
+ if (lastInterleaved !== null) {
625
+ let interleaved = lastInterleaved;
626
+ do {
627
+ newLanes = mergeLanes(newLanes, interleaved.lane);
628
+ interleaved = ((interleaved: any).next: Update<State>);
629
+ } while (interleaved !== lastInterleaved);
630
+ } else if (firstBaseUpdate === null) {
631
// `queue.lanes` is used for entangling transitions. We can set it back to
632
// zero once the queue is empty.
633
queue.shared.lanes = NoLanes;
packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js
+10
-1
@@ -320,7 +320,16 @@ describe('ReactIncrementalUpdates', () => {
320
});
321
322
expect(instance.state).toEqual({a: 'a', b: 'b'});
323
- expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']);
323
+
324
+ if (gate(flags => flags.deferRenderPhaseUpdateToNextBatch)) {
325
+ expect(Scheduler).toHaveYielded([
326
+ 'componentWillReceiveProps',
327
+ 'render',
328
+ 'render',
329
+ ]);
330
+ } else {
331
+ expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']);
332
+ }
333
});
334
335
it('updates triggered from inside a class setState updater', () => {