-
Notifications
You must be signed in to change notification settings - Fork 51k
Expand file tree
/
Copy pathReactFiberConcurrentUpdates.js
More file actions
287 lines (259 loc) · 10.1 KB
/
ReactFiberConcurrentUpdates.js
File metadata and controls
287 lines (259 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {
UpdateQueue as HookQueue,
Update as HookUpdate,
} from './ReactFiberHooks';
import type {
SharedQueue as ClassQueue,
Update as ClassUpdate,
} from './ReactFiberClassUpdateQueue';
import type {Lane, Lanes} from './ReactFiberLane';
import type {OffscreenInstance} from './ReactFiberOffscreenComponent';
import {
warnAboutUpdateOnNotYetMountedFiberInDEV,
throwIfInfiniteUpdateLoopDetected,
getWorkInProgressRoot,
} from './ReactFiberWorkLoop';
import {NoLane, NoLanes, mergeLanes, markHiddenUpdate} from './ReactFiberLane';
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
import {HostRoot, OffscreenComponent} from './ReactWorkTags';
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
export type ConcurrentUpdate = {
next: ConcurrentUpdate,
lane: Lane,
};
type ConcurrentQueue = {
pending: ConcurrentUpdate | null,
};
// If a render is in progress, and we receive an update from a concurrent event,
// we wait until the current render is over (either finished or interrupted)
// before adding it to the fiber/hook queue. Push to this array so we can
// access the queue, fiber, update, et al later.
const concurrentQueues: Array<any> = [];
let concurrentQueuesIndex = 0;
let concurrentlyUpdatedLanes: Lanes = NoLanes;
export function finishQueueingConcurrentUpdates(): void {
const endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0;
concurrentlyUpdatedLanes = NoLanes;
let i = 0;
while (i < endIndex) {
const fiber: Fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
const queue: ConcurrentQueue = concurrentQueues[i];
concurrentQueues[i++] = null;
const update: ConcurrentUpdate = concurrentQueues[i];
concurrentQueues[i++] = null;
const lane: Lane = concurrentQueues[i];
concurrentQueues[i++] = null;
if (queue !== null && update !== null) {
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
if (lane !== NoLane) {
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
}
export function getConcurrentlyUpdatedLanes(): Lanes {
return concurrentlyUpdatedLanes;
}
function enqueueUpdate(
fiber: Fiber,
queue: ConcurrentQueue | null,
update: ConcurrentUpdate | null,
lane: Lane,
) {
// Don't update the `childLanes` on the return path yet. If we already in
// the middle of rendering, wait until after it has completed.
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
// The fiber's `lane` field is used in some places to check if any work is
// scheduled, to perform an eager bailout, so we need to update it immediately.
// TODO: We should probably move this to the "shared" queue instead.
fiber.lanes = mergeLanes(fiber.lanes, lane);
const alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
export function enqueueConcurrentHookUpdateAndEagerlyBailout<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
): void {
// This function is used to queue an update that doesn't need a rerender. The
// only reason we queue it is in case there's a subsequent higher priority
// update that causes it to be rebased.
const lane = NoLane;
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
// Usually we can rely on the upcoming render phase to process the concurrent
// queue. However, since this is a bail out, we're not scheduling any work
// here. So the update we just queued will leak until something else happens
// to schedule work (if ever).
//
// Check if we're currently in the middle of rendering a tree, and if not,
// process the queue immediately to prevent a leak.
const isConcurrentlyRendering = getWorkInProgressRoot() !== null;
if (!isConcurrentlyRendering) {
finishQueueingConcurrentUpdates();
}
}
export function enqueueConcurrentClassUpdate<State>(
fiber: Fiber,
queue: ClassQueue<State>,
update: ClassUpdate<State>,
lane: Lane,
): FiberRoot | null {
const concurrentQueue: ConcurrentQueue = (queue: any);
const concurrentUpdate: ConcurrentUpdate = (update: any);
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
export function enqueueConcurrentRenderForLane(
fiber: Fiber,
lane: Lane,
): FiberRoot | null {
enqueueUpdate(fiber, null, null, lane);
return getRootForUpdatedFiber(fiber);
}
// Calling this function outside this module should only be done for backwards
// compatibility and should always be accompanied by a warning.
export function unsafe_markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
// NOTE: For Hyrum's Law reasons, if an infinite update loop is detected, it
// should throw before `markUpdateLaneFromFiberToRoot` is called. But this is
// undefined behavior and we can change it if we need to; it just so happens
// that, at the time of this writing, there's an internal product test that
// happens to rely on this.
const root = getRootForUpdatedFiber(sourceFiber);
markUpdateLaneFromFiberToRoot(sourceFiber, null, lane);
return root;
}
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
update: ConcurrentUpdate | null,
lane: Lane,
): null | FiberRoot {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// Walk the parent path to the root and update the child lanes.
let isHidden = false;
let parent = sourceFiber.return;
let node = sourceFiber;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
if (parent.tag === OffscreenComponent) {
// Check if this offscreen boundary is currently hidden.
//
// The instance may be null if the Offscreen parent was unmounted. Usually
// the parent wouldn't be reachable in that case because we disconnect
// fibers from the tree when they are deleted. However, there's a weird
// edge case where setState is called on a fiber that was interrupted
// before it ever mounted. Because it never mounts, it also never gets
// deleted. Because it never gets deleted, its return pointer never gets
// disconnected. Which means it may be attached to a deleted Offscreen
// parent node. (This discovery suggests it may be better for memory usage
// if we don't attach the `return` pointer until the commit phase, though
// in order to do that we'd need some other way to track the return
// pointer during the initial render, like on the stack.)
//
// This case is always accompanied by a warning, but we still need to
// account for it. (There may be other cases that we haven't discovered,
// too.)
const offscreenInstance: OffscreenInstance | null = parent.stateNode;
if (
offscreenInstance !== null &&
!(offscreenInstance._visibility & OffscreenVisible)
) {
isHidden = true;
}
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
if (isHidden && update !== null) {
markHiddenUpdate(root, update, lane);
}
return root;
}
return null;
}
function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
// TODO: We will detect and infinite update loop and throw even if this fiber
// has already unmounted. This isn't really necessary but it happens to be the
// current behavior we've used for several release cycles. Consider not
// performing this check if the updated fiber already unmounted, since it's
// not possible for that to cause an infinite update loop.
throwIfInfiniteUpdateLoopDetected(false);
// When a setState happens, we must ensure the root is scheduled. Because
// update queues do not have a backpointer to the root, the only way to do
// this currently is to walk up the return path. This used to not be a big
// deal because we would have to walk up the return path to set
// the `childLanes`, anyway, but now those two traversals happen at
// different times.
// TODO: Consider adding a `root` backpointer on the update queue.
detectUpdateOnUnmountedFiber(sourceFiber, sourceFiber);
let node = sourceFiber;
let parent = node.return;
while (parent !== null) {
detectUpdateOnUnmountedFiber(sourceFiber, node);
node = parent;
parent = node.return;
}
return node.tag === HostRoot ? (node.stateNode: FiberRoot) : null;
}
function detectUpdateOnUnmountedFiber(sourceFiber: Fiber, parent: Fiber) {
if (__DEV__) {
const alternate = parent.alternate;
if (
alternate === null &&
(parent.flags & (Placement | Hydrating)) !== NoFlags
) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}