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