Skip to content

Commit 935574b

Browse files
committed
Don't update childLanes until after current render
(This is the riskiest commit in the stack.) Updates that occur in a concurrent event while a render is already in progress can't be processed during that render. This is tricky to get right. Previously we solved this by adding concurrent updates to a special `interleaved` queue, then transferring the `interleaved` queue to the `pending` queue after the render phase had completed. However, we would still mutate the `childLanes` along the parent path immediately, which can lead to its own subtle data races. Instead, we can queue the entire operation until after the render phase has completed. This replaces the need for an `interleaved` field on every fiber/hook queue. The main motivation for this change, aside from simplifying the logic a bit, is so we can read information about the current fiber while we're walking up its return path, like whether it's inside a hidden tree. (I haven't done anything like that in this commit, though.)
1 parent 0dd20c3 commit 935574b

8 files changed

+320
-300
lines changed

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ export type Update<State> = {|
132132

133133
export type SharedQueue<State> = {|
134134
pending: Update<State> | null,
135-
interleaved: Update<State> | null,
136135
lanes: Lanes,
137136
|};
138137

@@ -172,7 +171,6 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
172171
lastBaseUpdate: null,
173172
shared: {
174173
pending: null,
175-
interleaved: null,
176174
lanes: NoLanes,
177175
},
178176
effects: null,
@@ -622,17 +620,7 @@ export function processUpdateQueue<State>(
622620
queue.firstBaseUpdate = newFirstBaseUpdate;
623621
queue.lastBaseUpdate = newLastBaseUpdate;
624622

625-
// Interleaved updates are stored on a separate queue. We aren't going to
626-
// process them during this render, but we do need to track which lanes
627-
// are remaining.
628-
const lastInterleaved = queue.shared.interleaved;
629-
if (lastInterleaved !== null) {
630-
let interleaved = lastInterleaved;
631-
do {
632-
newLanes = mergeLanes(newLanes, interleaved.lane);
633-
interleaved = ((interleaved: any).next: Update<State>);
634-
} while (interleaved !== lastInterleaved);
635-
} else if (firstBaseUpdate === null) {
623+
if (firstBaseUpdate === null) {
636624
// `queue.lanes` is used for entangling transitions. We can set it back to
637625
// zero once the queue is empty.
638626
queue.shared.lanes = NoLanes;

packages/react-reconciler/src/ReactFiberClassUpdateQueue.old.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ export type Update<State> = {|
132132

133133
export type SharedQueue<State> = {|
134134
pending: Update<State> | null,
135-
interleaved: Update<State> | null,
136135
lanes: Lanes,
137136
|};
138137

@@ -172,7 +171,6 @@ export function initializeUpdateQueue<State>(fiber: Fiber): void {
172171
lastBaseUpdate: null,
173172
shared: {
174173
pending: null,
175-
interleaved: null,
176174
lanes: NoLanes,
177175
},
178176
effects: null,
@@ -622,17 +620,7 @@ export function processUpdateQueue<State>(
622620
queue.firstBaseUpdate = newFirstBaseUpdate;
623621
queue.lastBaseUpdate = newLastBaseUpdate;
624622

625-
// Interleaved updates are stored on a separate queue. We aren't going to
626-
// process them during this render, but we do need to track which lanes
627-
// are remaining.
628-
const lastInterleaved = queue.shared.interleaved;
629-
if (lastInterleaved !== null) {
630-
let interleaved = lastInterleaved;
631-
do {
632-
newLanes = mergeLanes(newLanes, interleaved.lane);
633-
interleaved = ((interleaved: any).next: Update<State>);
634-
} while (interleaved !== lastInterleaved);
635-
} else if (firstBaseUpdate === null) {
623+
if (firstBaseUpdate === null) {
636624
// `queue.lanes` is used for entangling transitions. We can set it back to
637625
// zero once the queue is empty.
638626
queue.shared.lanes = NoLanes;

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

Lines changed: 148 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -16,169 +16,208 @@ import type {
1616
SharedQueue as ClassQueue,
1717
Update as ClassUpdate,
1818
} from './ReactFiberClassUpdateQueue.new';
19-
import type {Lane} from './ReactFiberLane.new';
19+
import type {Lane, Lanes} from './ReactFiberLane.new';
2020

2121
import {warnAboutUpdateOnNotYetMountedFiberInDEV} from './ReactFiberWorkLoop.new';
22-
import {mergeLanes} from './ReactFiberLane.new';
22+
import {
23+
NoLane,
24+
NoLanes,
25+
mergeLanes,
26+
isSubsetOfLanes,
27+
} from './ReactFiberLane.new';
2328
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
2429
import {HostRoot} from './ReactWorkTags';
2530

26-
// An array of all update queues that received updates during the current
27-
// render. When this render exits, either because it finishes or because it is
28-
// interrupted, the interleaved updates will be transferred onto the main part
29-
// of the queue.
30-
let concurrentQueues: Array<
31-
HookQueue<any, any> | ClassQueue<any>,
32-
> | null = null;
31+
type ConcurrentUpdate = {
32+
next: ConcurrentUpdate,
33+
};
3334

34-
export function pushConcurrentUpdateQueue(
35-
queue: HookQueue<any, any> | ClassQueue<any>,
36-
) {
37-
if (concurrentQueues === null) {
38-
concurrentQueues = [queue];
39-
} else {
40-
concurrentQueues.push(queue);
41-
}
42-
}
35+
type ConcurrentQueue = {
36+
pending: ConcurrentUpdate | null,
37+
};
38+
39+
// If a render is in progress, and we receive an update from a concurrent event,
40+
// we wait until the current render is over (either finished or interrupted)
41+
// before adding it to the fiber/hook queue. Push to this array so we can
42+
// access the queue, fiber, update, et al later.
43+
const concurrentQueues: Array<any> = [];
44+
let concurrentQueuesIndex = 0;
4345

44-
export function finishQueueingConcurrentUpdates() {
45-
// Transfer the interleaved updates onto the main queue. Each queue has a
46-
// `pending` field and an `interleaved` field. When they are not null, they
47-
// point to the last node in a circular linked list. We need to append the
48-
// interleaved list to the end of the pending list by joining them into a
49-
// single, circular list.
50-
if (concurrentQueues !== null) {
51-
for (let i = 0; i < concurrentQueues.length; i++) {
52-
const queue = concurrentQueues[i];
53-
const lastInterleavedUpdate = queue.interleaved;
54-
if (lastInterleavedUpdate !== null) {
55-
queue.interleaved = null;
56-
const firstInterleavedUpdate = lastInterleavedUpdate.next;
57-
const lastPendingUpdate = queue.pending;
58-
if (lastPendingUpdate !== null) {
59-
const firstPendingUpdate = lastPendingUpdate.next;
60-
lastPendingUpdate.next = (firstInterleavedUpdate: any);
61-
lastInterleavedUpdate.next = (firstPendingUpdate: any);
62-
}
63-
queue.pending = (lastInterleavedUpdate: any);
46+
export function finishQueueingConcurrentUpdates(): Lanes {
47+
const endIndex = concurrentQueuesIndex;
48+
concurrentQueuesIndex = 0;
49+
50+
let lanes = NoLanes;
51+
52+
let i = 0;
53+
while (i < endIndex) {
54+
const fiber: Fiber = concurrentQueues[i];
55+
concurrentQueues[i++] = null;
56+
const queue: ConcurrentQueue = concurrentQueues[i];
57+
concurrentQueues[i++] = null;
58+
const update: ConcurrentUpdate = concurrentQueues[i];
59+
concurrentQueues[i++] = null;
60+
const lane: Lane = concurrentQueues[i];
61+
concurrentQueues[i++] = null;
62+
63+
if (queue !== null && update !== null) {
64+
const pending = queue.pending;
65+
if (pending === null) {
66+
// This is the first update. Create a circular list.
67+
update.next = update;
68+
} else {
69+
update.next = pending.next;
70+
pending.next = update;
6471
}
72+
queue.pending = update;
73+
}
74+
75+
if (lane !== NoLane) {
76+
lanes = mergeLanes(lanes, lane);
77+
markUpdateLaneFromFiberToRoot(fiber, lane);
6578
}
66-
concurrentQueues = null;
6779
}
80+
81+
return lanes;
6882
}
6983

70-
export function enqueueConcurrentHookUpdate<S, A>(
84+
function enqueueUpdate(
7185
fiber: Fiber,
72-
queue: HookQueue<S, A>,
73-
update: HookUpdate<S, A>,
86+
queue: ConcurrentQueue | null,
87+
update: ConcurrentUpdate | null,
7488
lane: Lane,
7589
) {
76-
const interleaved = queue.interleaved;
77-
if (interleaved === null) {
78-
// This is the first update. Create a circular list.
79-
update.next = update;
80-
// At the end of the current render, this queue's interleaved updates will
81-
// be transferred to the pending queue.
82-
pushConcurrentUpdateQueue(queue);
83-
} else {
84-
update.next = interleaved.next;
85-
interleaved.next = update;
90+
// Don't update the `childLanes` on the return path yet. If we already in
91+
// the middle of rendering, wait until after it has completed.
92+
concurrentQueues[concurrentQueuesIndex++] = fiber;
93+
concurrentQueues[concurrentQueuesIndex++] = queue;
94+
concurrentQueues[concurrentQueuesIndex++] = update;
95+
concurrentQueues[concurrentQueuesIndex++] = lane;
96+
97+
// The fiber's `lane` field is used in some places to check if any work is
98+
// scheduled, to perform an eager bailout, so we need to update it immediately.
99+
// TODO: We should probably move this to the "shared" queue instead.
100+
fiber.lanes = mergeLanes(fiber.lanes, lane);
101+
const alternate = fiber.alternate;
102+
if (alternate !== null) {
103+
alternate.lanes = mergeLanes(alternate.lanes, lane);
86104
}
87-
queue.interleaved = update;
105+
}
88106

89-
return markUpdateLaneFromFiberToRoot(fiber, lane);
107+
export function enqueueConcurrentHookUpdate<S, A>(
108+
fiber: Fiber,
109+
queue: HookQueue<S, A>,
110+
update: HookUpdate<S, A>,
111+
lane: Lane,
112+
): FiberRoot | null {
113+
const concurrentQueue: ConcurrentQueue = (queue: any);
114+
const concurrentUpdate: ConcurrentUpdate = (update: any);
115+
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
116+
return getRootForUpdatedFiber(fiber);
90117
}
91118

92119
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
93120
fiber: Fiber,
94121
queue: HookQueue<S, A>,
95122
update: HookUpdate<S, A>,
96-
lane: Lane,
97123
): void {
98-
const interleaved = queue.interleaved;
99-
if (interleaved === null) {
100-
// This is the first update. Create a circular list.
101-
update.next = update;
102-
// At the end of the current render, this queue's interleaved updates will
103-
// be transferred to the pending queue.
104-
pushConcurrentUpdateQueue(queue);
105-
} else {
106-
update.next = interleaved.next;
107-
interleaved.next = update;
108-
}
109-
queue.interleaved = update;
124+
// This function is used to queue an update that doesn't need a rerender. The
125+
// only reason we queue it is in case there's a subsequent higher priority
126+
// update that causes it to be rebased.
127+
const lane = NoLane;
128+
const concurrentQueue: ConcurrentQueue = (queue: any);
129+
const concurrentUpdate: ConcurrentUpdate = (update: any);
130+
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
110131
}
111132

112133
export function enqueueConcurrentClassUpdate<State>(
113134
fiber: Fiber,
114135
queue: ClassQueue<State>,
115136
update: ClassUpdate<State>,
116137
lane: Lane,
117-
) {
118-
const interleaved = queue.interleaved;
119-
if (interleaved === null) {
120-
// This is the first update. Create a circular list.
121-
update.next = update;
122-
// At the end of the current render, this queue's interleaved updates will
123-
// be transferred to the pending queue.
124-
pushConcurrentUpdateQueue(queue);
125-
} else {
126-
update.next = interleaved.next;
127-
interleaved.next = update;
128-
}
129-
queue.interleaved = update;
130-
131-
return markUpdateLaneFromFiberToRoot(fiber, lane);
138+
): FiberRoot | null {
139+
const concurrentQueue: ConcurrentQueue = (queue: any);
140+
const concurrentUpdate: ConcurrentUpdate = (update: any);
141+
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
142+
return getRootForUpdatedFiber(fiber);
132143
}
133144

134-
export function enqueueConcurrentRenderForLane(fiber: Fiber, lane: Lane) {
135-
return markUpdateLaneFromFiberToRoot(fiber, lane);
145+
export function enqueueConcurrentRenderForLane(
146+
fiber: Fiber,
147+
lane: Lane,
148+
): FiberRoot | null {
149+
enqueueUpdate(fiber, null, null, lane);
150+
return getRootForUpdatedFiber(fiber);
136151
}
137152

138153
// Calling this function outside this module should only be done for backwards
139154
// compatibility and should always be accompanied by a warning.
140-
export const unsafe_markUpdateLaneFromFiberToRoot = markUpdateLaneFromFiberToRoot;
141-
142-
function markUpdateLaneFromFiberToRoot(
155+
export function unsafe_markUpdateLaneFromFiberToRoot(
143156
sourceFiber: Fiber,
144157
lane: Lane,
145158
): FiberRoot | null {
159+
markUpdateLaneFromFiberToRoot(sourceFiber, lane);
160+
return getRootForUpdatedFiber(sourceFiber);
161+
}
162+
163+
function markUpdateLaneFromFiberToRoot(sourceFiber: Fiber, lane: Lane): void {
146164
// Update the source fiber's lanes
147165
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
148166
let alternate = sourceFiber.alternate;
149167
if (alternate !== null) {
150168
alternate.lanes = mergeLanes(alternate.lanes, lane);
151169
}
152-
if (__DEV__) {
153-
if (
154-
alternate === null &&
155-
(sourceFiber.flags & (Placement | Hydrating)) !== NoFlags
156-
) {
157-
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
158-
}
159-
}
160170
// Walk the parent path to the root and update the child lanes.
161-
let node = sourceFiber;
162171
let parent = sourceFiber.return;
163172
while (parent !== null) {
164-
parent.childLanes = mergeLanes(parent.childLanes, lane);
165173
alternate = parent.alternate;
166-
if (alternate !== null) {
167-
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
174+
if (isSubsetOfLanes(parent.childLanes, lane)) {
175+
if (alternate === null || isSubsetOfLanes(alternate.childLanes, lane)) {
176+
// Both fibers already have sufficient priority. Don't need to update
177+
// the rest of the return path. This is a helpful optimization in the
178+
// case where the same component is updated many times in rapid
179+
// succession, or even in the same event.
180+
return;
181+
} else {
182+
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
183+
}
168184
} else {
169-
if (__DEV__) {
170-
if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
171-
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
172-
}
185+
parent.childLanes = mergeLanes(parent.childLanes, lane);
186+
if (alternate !== null) {
187+
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
173188
}
174189
}
175-
node = parent;
176190
parent = parent.return;
177191
}
178-
if (node.tag === HostRoot) {
179-
const root: FiberRoot = node.stateNode;
180-
return root;
181-
} else {
182-
return null;
192+
}
193+
194+
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
195+
// When a setState happens, we must ensure the root is scheduled. Because
196+
// update queues do not have a backpointer to the root, the only way to do
197+
// this currently is to walk up the return path. This used to not be a big
198+
// deal because we would have to walk up the return path to set
199+
// the `childLanes`, anyway, but now those two traversals happen at
200+
// different times.
201+
// TODO: Consider adding a `root` backpointer on the update queue.
202+
detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
203+
let node = sourceFiber;
204+
let parent = node.return;
205+
while (parent !== null) {
206+
detectUpdateOnUnmountedFiber(sourceFiber, node);
207+
node = parent;
208+
parent = node.return;
209+
}
210+
return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
211+
}
212+
213+
function detectUpdateOnUnmountedFiber(sourceFiber: Fiber, parent: Fiber) {
214+
if (__DEV__) {
215+
const alternate = parent.alternate;
216+
if (
217+
alternate === null &&
218+
(parent.flags & (Placement | Hydrating)) !== NoFlags
219+
) {
220+
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
221+
}
183222
}
184223
}

0 commit comments

Comments
 (0)