/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-reconciler/src/ReactFiberTransition.js
353 строки
12 KB
Sebastian Markbåge
[Fiber] Adjust the suspensey image/css timeout based on already elapsed time (#34478)
15 сен 2025, 23:05
Не верифицирован
15 сен 2025, 23:05
e3f1918
Код
Авторство
О чём код?
/** * 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 { Thenable, GestureProvider, GestureOptions, } from 'shared/ReactTypes'; import {NoLane, type Lanes} from './ReactFiberLane'; import type {StackCursor} from './ReactFiberStack'; import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent'; import type {Transition} from 'react/src/ReactStartTransition'; import type {ScheduledGesture} from './ReactFiberGestureScheduler'; import { enableTransitionTracing, enableViewTransition, enableGestureTransition, } from 'shared/ReactFeatureFlags'; import {isPrimaryRenderer} from './ReactFiberConfig'; import {createCursor, push, pop} from './ReactFiberStack'; import { getWorkInProgressRoot, getWorkInProgressTransitions, markTransitionStarted, } from './ReactFiberWorkLoop'; import { createCache, retainCache, CacheContext, } from './ReactFiberCacheComponent'; import { queueTransitionTypes, entangleAsyncTransitionTypes, entangledTransitionTypes, } from './ReactFiberTransitionTypes'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { entangleAsyncAction, peekEntangledActionLane, } from './ReactFiberAsyncAction'; import {startAsyncTransitionTimer} from './ReactProfilerTimer'; import {firstScheduledRoot} from './ReactFiberRootScheduler'; import { startScheduledGesture, cancelScheduledGesture, } from './ReactFiberGestureScheduler'; export const NoTransition = null; // Attach this reconciler instance's onStartTransitionFinish implementation to // the shared internals object. This is used by the isomorphic implementation of // startTransition to compose all the startTransitions together. // // function startTransition(fn) { // return startTransitionDOM(() => { // return startTransitionART(() => { // return startTransitionThreeFiber(() => { // // and so on... // return fn(); // }); // }); // }); // } // // Currently we only compose together the code that runs at the end of each // startTransition, because for now that's sufficient — the part that sets // isTransition=true on the stack uses a separate shared internal field. But // really we should delete the shared field and track isTransition per // reconciler. Leaving this for a future PR. const prevOnStartTransitionFinish = ReactSharedInternals.S; ReactSharedInternals.S = function onStartTransitionFinishForReconciler( transition: Transition, returnValue: mixed, ) { markTransitionStarted(); if ( typeof returnValue === 'object' && returnValue !== null && typeof returnValue.then === 'function' ) { // If we're going to wait on some async work before scheduling an update. // We mark the time so we can later log how long we were blocked on the Action. // Ideally, we'd include the sync part of the action too but since that starts // in isomorphic code it currently leads to tricky layering. We'd have to pass // in performance.now() to this callback but we sometimes use a polyfill. startAsyncTransitionTimer(); // This is an async action const thenable: Thenable<mixed> = (returnValue: any); entangleAsyncAction(transition, thenable); } if (enableViewTransition) { if (entangledTransitionTypes !== null) { // If we scheduled work on any new roots, we need to add any entangled async // transition types to those roots too. let root = firstScheduledRoot; while (root !== null) { queueTransitionTypes(root, entangledTransitionTypes); root = root.next; } } const transitionTypes = transition.types; if (transitionTypes !== null) { // Within this Transition we should've now scheduled any roots we have updates // to work on. If there are no updates on a root, then the Transition type won't // be applied to that root. let root = firstScheduledRoot; while (root !== null) { queueTransitionTypes(root, transitionTypes); root = root.next; } if (peekEntangledActionLane() !== NoLane) { // If we have entangled, async actions going on, the update associated with // these types might come later. We need to save them for later. entangleAsyncTransitionTypes(transitionTypes); } } } if (prevOnStartTransitionFinish !== null) { prevOnStartTransitionFinish(transition, returnValue); } }; function chainGestureCancellation( root: FiberRoot, scheduledGesture: ScheduledGesture, prevCancel: null | (() => void), ): () => void { return function cancelGesture(): void { if (scheduledGesture !== null) { cancelScheduledGesture(root, scheduledGesture); } if (prevCancel !== null) { prevCancel(); } }; } if (enableGestureTransition) { const prevOnStartGestureTransitionFinish = ReactSharedInternals.G; ReactSharedInternals.G = function onStartGestureTransitionFinishForReconciler( transition: Transition, provider: GestureProvider, options: ?GestureOptions, ): () => void { let cancel = null; if (prevOnStartGestureTransitionFinish !== null) { cancel = prevOnStartGestureTransitionFinish( transition, provider, options, ); } // For every root that has work scheduled, check if there's a ScheduledGesture // matching this provider and if so, increase its ref count so its retained by // this cancellation callback. We could add the roots to a temporary array as // we schedule them inside the callback to keep track of them. There's a slight // nuance here which is that if there's more than one root scheduled with the // same provider, but it doesn't update in this callback, then we still update // its options and retain it until this cancellation releases. The idea being // that it's conceptually started globally. let root = firstScheduledRoot; while (root !== null) { const scheduledGesture = startScheduledGesture( root, provider, options, transition.types, ); if (scheduledGesture !== null) { cancel = chainGestureCancellation(root, scheduledGesture, cancel); } root = root.next; } if (cancel !== null) { return cancel; } return function cancelGesture(): void { // Nothing was scheduled but it could've been scheduled by another renderer. }; }; } export function requestCurrentTransition(): Transition | null { return ReactSharedInternals.T; } // When retrying a Suspense/Offscreen boundary, we restore the cache that was // used during the previous render by placing it here, on the stack. const resumedCache: StackCursor<Cache | null> = createCursor(null); // During the render/synchronous commit phase, we don't actually process the // transitions. Therefore, we want to lazily combine transitions. Instead of // comparing the arrays of transitions when we combine them and storing them // and filtering out the duplicates, we will instead store the unprocessed transitions // in an array and actually filter them in the passive phase. const transitionStack: StackCursor<Array<Transition> | null> = createCursor(null); function peekCacheFromPool(): Cache | null { // Check if the cache pool already has a cache we can use. // If we're rendering inside a Suspense boundary that is currently hidden, // we should use the same cache that we used during the previous render, if // one exists. const cacheResumedFromPreviousRender = resumedCache.current; if (cacheResumedFromPreviousRender !== null) { return cacheResumedFromPreviousRender; } // Otherwise, check the root's cache pool. const root = (getWorkInProgressRoot(): any); const cacheFromRootCachePool = root.pooledCache; return cacheFromRootCachePool; } export function requestCacheFromPool(renderLanes: Lanes): Cache { // Similar to previous function, except if there's not already a cache in the // pool, we allocate a new one. const cacheFromPool = peekCacheFromPool(); if (cacheFromPool !== null) { return cacheFromPool; } // Create a fresh cache and add it to the root cache pool. A cache can have // multiple owners: // - A cache pool that lives on the FiberRoot. This is where all fresh caches // are originally created (TODO: except during refreshes, until we implement // this correctly). The root takes ownership immediately when the cache is // created. Conceptually, root.pooledCache is an Option<Arc<Cache>> (owned), // and the return value of this function is a &Arc<Cache> (borrowed). // - One of several fiber types: host root, cache boundary, suspense // component. These retain and release in the commit phase. const root = (getWorkInProgressRoot(): any); const freshCache = createCache(); root.pooledCache = freshCache; retainCache(freshCache); if (freshCache !== null) { root.pooledCacheLanes |= renderLanes; } return freshCache; } export function pushRootTransition( workInProgress: Fiber, root: FiberRoot, renderLanes: Lanes, ) { if (enableTransitionTracing) { const rootTransitions = getWorkInProgressTransitions(); push(transitionStack, rootTransitions, workInProgress); } } export function popRootTransition( workInProgress: Fiber, root: FiberRoot, renderLanes: Lanes, ) { if (enableTransitionTracing) { pop(transitionStack, workInProgress); } } export function pushTransition( offscreenWorkInProgress: Fiber, prevCachePool: SpawnedCachePool | null, newTransitions: Array<Transition> | null, ): void { if (prevCachePool === null) { push(resumedCache, resumedCache.current, offscreenWorkInProgress); } else { push(resumedCache, prevCachePool.pool, offscreenWorkInProgress); } if (enableTransitionTracing) { if (transitionStack.current === null) { push(transitionStack, newTransitions, offscreenWorkInProgress); } else if (newTransitions === null) { push(transitionStack, transitionStack.current, offscreenWorkInProgress); } else { push( transitionStack, transitionStack.current.concat(newTransitions), offscreenWorkInProgress, ); } } } export function popTransition(workInProgress: Fiber, current: Fiber | null) { if (current !== null) { if (enableTransitionTracing) { pop(transitionStack, workInProgress); } pop(resumedCache, workInProgress); } } export function getPendingTransitions(): Array<Transition> | null { if (!enableTransitionTracing) { return null; } return transitionStack.current; } export function getSuspendedCache(): SpawnedCachePool | null { // This function is called when a Suspense boundary suspends. It returns the // cache that would have been used to render fresh data during this render, // if there was any, so that we can resume rendering with the same cache when // we receive more data. const cacheFromPool = peekCacheFromPool(); if (cacheFromPool === null) { return null; } return { // We must also save the parent, so that when we resume we can detect // a refresh. parent: isPrimaryRenderer ? CacheContext._currentValue : CacheContext._currentValue2, pool: cacheFromPool, }; } export function getOffscreenDeferredCache(): SpawnedCachePool | null { const cacheFromPool = peekCacheFromPool(); if (cacheFromPool === null) { return null; } return { // We must also store the parent, so that when we resume we can detect // a refresh. parent: isPrimaryRenderer ? CacheContext._currentValue : CacheContext._currentValue2, pool: cacheFromPool, }; }