| 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 | } |