main
js 357 lines 11.8 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 import type {Fiber, FiberRoot} from './ReactInternalTypes';
10 import type {
11 Thenable,
12 GestureProvider,
13 GestureOptions,
14 } from 'shared/ReactTypes';
15 import {NoLane, type Lanes} from './ReactFiberLane';
16 import type {StackCursor} from './ReactFiberStack';
17 import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent';
18 import type {Transition} from 'react/src/ReactStartTransition';
19 import type {ScheduledGesture} from './ReactFiberGestureScheduler';
20
21 import {
22 enableTransitionTracing,
23 enableViewTransition,
24 enableGestureTransition,
25 } from 'shared/ReactFeatureFlags';
26 import {isPrimaryRenderer} from './ReactFiberConfig';
27 import {createCursor, push, pop} from './ReactFiberStack';
28 import {
29 getWorkInProgressRoot,
30 getWorkInProgressTransitions,
31 markTransitionStarted,
32 } from './ReactFiberWorkLoop';
33 import {
34 createCache,
35 retainCache,
36 CacheContext,
37 } from './ReactFiberCacheComponent';
38 import {
39 queueTransitionTypes,
40 entangleAsyncTransitionTypes,
41 entangledTransitionTypes,
42 } from './ReactFiberTransitionTypes';
43
44 import ReactSharedInternals from 'shared/ReactSharedInternals';
45 import {
46 entangleAsyncAction,
47 peekEntangledActionLane,
48 } from './ReactFiberAsyncAction';
49 import {startAsyncTransitionTimer} from './ReactProfilerTimer';
50 import {firstScheduledRoot} from './ReactFiberRootScheduler';
51 import {
52 startScheduledGesture,
53 cancelScheduledGesture,
54 } from './ReactFiberGestureScheduler';
55
56 export const NoTransition = null;
57
58 // Attach this reconciler instance's onStartTransitionFinish implementation to
59 // the shared internals object. This is used by the isomorphic implementation of
60 // startTransition to compose all the startTransitions together.
61 //
62 // function startTransition(fn) {
63 // return startTransitionDOM(() => {
64 // return startTransitionART(() => {
65 // return startTransitionThreeFiber(() => {
66 // // and so on...
67 // return fn();
68 // });
69 // });
70 // });
71 // }
72 //
73 // Currently we only compose together the code that runs at the end of each
74 // startTransition, because for now that's sufficient — the part that sets
75 // isTransition=true on the stack uses a separate shared internal field. But
76 // really we should delete the shared field and track isTransition per
77 // reconciler. Leaving this for a future PR.
78 const prevOnStartTransitionFinish = ReactSharedInternals.S;
79 ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
80 transition: Transition,
81 returnValue: mixed,
82 ) {
83 markTransitionStarted();
84 if (
85 typeof returnValue === 'object' &&
86 returnValue !== null &&
87 typeof returnValue.then === 'function'
88 ) {
89 // If we're going to wait on some async work before scheduling an update.
90 // We mark the time so we can later log how long we were blocked on the Action.
91 // Ideally, we'd include the sync part of the action too but since that starts
92 // in isomorphic code it currently leads to tricky layering. We'd have to pass
93 // in performance.now() to this callback but we sometimes use a polyfill.
94 startAsyncTransitionTimer();
95
96 // This is an async action
97 const thenable: Thenable<mixed> = returnValue as any;
98 entangleAsyncAction(transition, thenable);
99 }
100 if (enableViewTransition) {
101 if (entangledTransitionTypes !== null) {
102 // If we scheduled work on any new roots, we need to add any entangled async
103 // transition types to those roots too.
104 let root = firstScheduledRoot;
105 while (root !== null) {
106 queueTransitionTypes(root, entangledTransitionTypes);
107 root = root.next;
108 }
109 }
110 const transitionTypes = transition.types;
111 if (transitionTypes !== null) {
112 // Within this Transition we should've now scheduled any roots we have updates
113 // to work on. If there are no updates on a root, then the Transition type won't
114 // be applied to that root.
115 let root = firstScheduledRoot;
116 while (root !== null) {
117 queueTransitionTypes(root, transitionTypes);
118 root = root.next;
119 }
120 if (peekEntangledActionLane() !== NoLane) {
121 // If we have entangled, async actions going on, the update associated with
122 // these types might come later. We need to save them for later.
123 entangleAsyncTransitionTypes(transitionTypes);
124 }
125 }
126 }
127 if (prevOnStartTransitionFinish !== null) {
128 prevOnStartTransitionFinish(transition, returnValue);
129 }
130 };
131
132 function chainGestureCancellation(
133 root: FiberRoot,
134 scheduledGesture: ScheduledGesture,
135 prevCancel: null | (() => void),
136 ): () => void {
137 return function cancelGesture(): void {
138 // $FlowFixMe[invalid-compare]
139 if (scheduledGesture !== null) {
140 cancelScheduledGesture(root, scheduledGesture);
141 }
142 if (prevCancel !== null) {
143 prevCancel();
144 }
145 };
146 }
147
148 if (enableGestureTransition) {
149 const prevOnStartGestureTransitionFinish = ReactSharedInternals.G;
150 ReactSharedInternals.G = function onStartGestureTransitionFinishForReconciler(
151 transition: Transition,
152 provider: GestureProvider,
153 options: ?GestureOptions,
154 ): () => void {
155 let cancel = null;
156 if (prevOnStartGestureTransitionFinish !== null) {
157 cancel = prevOnStartGestureTransitionFinish(
158 transition,
159 provider,
160 options,
161 );
162 }
163 // For every root that has work scheduled, check if there's a ScheduledGesture
164 // matching this provider and if so, increase its ref count so its retained by
165 // this cancellation callback. We could add the roots to a temporary array as
166 // we schedule them inside the callback to keep track of them. There's a slight
167 // nuance here which is that if there's more than one root scheduled with the
168 // same provider, but it doesn't update in this callback, then we still update
169 // its options and retain it until this cancellation releases. The idea being
170 // that it's conceptually started globally.
171 let root = firstScheduledRoot;
172 while (root !== null) {
173 const scheduledGesture = startScheduledGesture(
174 root,
175 provider,
176 options,
177 transition.types,
178 );
179 if (scheduledGesture !== null) {
180 cancel = chainGestureCancellation(root, scheduledGesture, cancel);
181 }
182 root = root.next;
183 }
184 if (cancel !== null) {
185 return cancel;
186 }
187 return function cancelGesture(): void {
188 // Nothing was scheduled but it could've been scheduled by another renderer.
189 };
190 };
191 }
192
193 export function requestCurrentTransition(): Transition | null {
194 return ReactSharedInternals.T;
195 }
196
197 // When retrying a Suspense/Offscreen boundary, we restore the cache that was
198 // used during the previous render by placing it here, on the stack.
199 const resumedCache: StackCursor<Cache | null> = createCursor(null);
200
201 // During the render/synchronous commit phase, we don't actually process the
202 // transitions. Therefore, we want to lazily combine transitions. Instead of
203 // comparing the arrays of transitions when we combine them and storing them
204 // and filtering out the duplicates, we will instead store the unprocessed transitions
205 // in an array and actually filter them in the passive phase.
206 const transitionStack: StackCursor<Array<Transition> | null> =
207 createCursor(null);
208
209 function peekCacheFromPool(): Cache | null {
210 // Check if the cache pool already has a cache we can use.
211
212 // If we're rendering inside a Suspense boundary that is currently hidden,
213 // we should use the same cache that we used during the previous render, if
214 // one exists.
215 const cacheResumedFromPreviousRender = resumedCache.current;
216 if (cacheResumedFromPreviousRender !== null) {
217 return cacheResumedFromPreviousRender;
218 }
219
220 // Otherwise, check the root's cache pool.
221 const root = getWorkInProgressRoot() as any;
222 const cacheFromRootCachePool = root.pooledCache;
223
224 return cacheFromRootCachePool;
225 }
226
227 export function requestCacheFromPool(renderLanes: Lanes): Cache {
228 // Similar to previous function, except if there's not already a cache in the
229 // pool, we allocate a new one.
230 const cacheFromPool = peekCacheFromPool();
231 if (cacheFromPool !== null) {
232 return cacheFromPool;
233 }
234
235 // Create a fresh cache and add it to the root cache pool. A cache can have
236 // multiple owners:
237 // - A cache pool that lives on the FiberRoot. This is where all fresh caches
238 // are originally created (TODO: except during refreshes, until we implement
239 // this correctly). The root takes ownership immediately when the cache is
240 // created. Conceptually, root.pooledCache is an Option<Arc<Cache>> (owned),
241 // and the return value of this function is a &Arc<Cache> (borrowed).
242 // - One of several fiber types: host root, cache boundary, suspense
243 // component. These retain and release in the commit phase.
244
245 const root = getWorkInProgressRoot() as any;
246 const freshCache = createCache();
247 root.pooledCache = freshCache;
248 retainCache(freshCache);
249 // $FlowFixMe[invalid-compare]
250 if (freshCache !== null) {
251 root.pooledCacheLanes |= renderLanes;
252 }
253 return freshCache;
254 }
255
256 export function pushRootTransition(
257 workInProgress: Fiber,
258 root: FiberRoot,
259 renderLanes: Lanes,
260 ) {
261 if (enableTransitionTracing) {
262 const rootTransitions = getWorkInProgressTransitions();
263 push(transitionStack, rootTransitions, workInProgress);
264 }
265 }
266
267 export function popRootTransition(
268 workInProgress: Fiber,
269 root: FiberRoot,
270 renderLanes: Lanes,
271 ) {
272 if (enableTransitionTracing) {
273 pop(transitionStack, workInProgress);
274 }
275 }
276
277 export function pushTransition(
278 offscreenWorkInProgress: Fiber,
279 prevCachePool: SpawnedCachePool | null,
280 newTransitions: Array<Transition> | null,
281 ): void {
282 if (prevCachePool === null) {
283 push(resumedCache, resumedCache.current, offscreenWorkInProgress);
284 } else {
285 push(resumedCache, prevCachePool.pool, offscreenWorkInProgress);
286 }
287
288 if (enableTransitionTracing) {
289 if (transitionStack.current === null) {
290 push(transitionStack, newTransitions, offscreenWorkInProgress);
291 } else if (newTransitions === null) {
292 push(transitionStack, transitionStack.current, offscreenWorkInProgress);
293 } else {
294 push(
295 transitionStack,
296 transitionStack.current.concat(newTransitions),
297 offscreenWorkInProgress,
298 );
299 }
300 }
301 }
302
303 export function popTransition(workInProgress: Fiber, current: Fiber | null) {
304 if (current !== null) {
305 if (enableTransitionTracing) {
306 pop(transitionStack, workInProgress);
307 }
308
309 pop(resumedCache, workInProgress);
310 }
311 }
312
313 export function getPendingTransitions(): Array<Transition> | null {
314 if (!enableTransitionTracing) {
315 return null;
316 }
317
318 return transitionStack.current;
319 }
320
321 export function getSuspendedCache(): SpawnedCachePool | null {
322 // This function is called when a Suspense boundary suspends. It returns the
323 // cache that would have been used to render fresh data during this render,
324 // if there was any, so that we can resume rendering with the same cache when
325 // we receive more data.
326 const cacheFromPool = peekCacheFromPool();
327 if (cacheFromPool === null) {
328 return null;
329 }
330
331 return {
332 // We must also save the parent, so that when we resume we can detect
333 // a refresh.
334 // $FlowFixMe[constant-condition]
335 parent: isPrimaryRenderer
336 ? CacheContext._currentValue
337 : CacheContext._currentValue2,
338 pool: cacheFromPool,
339 };
340 }
341
342 export function getOffscreenDeferredCache(): SpawnedCachePool | null {
343 const cacheFromPool = peekCacheFromPool();
344 if (cacheFromPool === null) {
345 return null;
346 }
347
348 return {
349 // We must also store the parent, so that when we resume we can detect
350 // a refresh.
351 // $FlowFixMe[constant-condition]
352 parent: isPrimaryRenderer
353 ? CacheContext._currentValue
354 : CacheContext._currentValue2,
355 pool: cacheFromPool,
356 };
357 }