| 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 {FiberRoot} from './ReactInternalTypes'; |
| 11 | import type {Lane, Lanes} from './ReactFiberLane'; |
| 12 | import type {PriorityLevel} from 'scheduler/src/SchedulerPriorities'; |
| 13 | import type {Transition} from 'react/src/ReactStartTransition'; |
| 14 | |
| 15 | import { |
| 16 | disableLegacyMode, |
| 17 | disableSchedulerTimeoutInWorkLoop, |
| 18 | enableProfilerTimer, |
| 19 | enableProfilerNestedUpdatePhase, |
| 20 | enableComponentPerformanceTrack, |
| 21 | enableYieldingBeforePassive, |
| 22 | enableGestureTransition, |
| 23 | enableDefaultTransitionIndicator, |
| 24 | } from 'shared/ReactFeatureFlags'; |
| 25 | import { |
| 26 | NoLane, |
| 27 | NoLanes, |
| 28 | SyncLane, |
| 29 | DefaultLane, |
| 30 | getHighestPriorityLane, |
| 31 | getNextLanes, |
| 32 | includesSyncLane, |
| 33 | markStarvedLanesAsExpired, |
| 34 | claimNextTransitionUpdateLane, |
| 35 | getNextLanesToFlushSync, |
| 36 | checkIfRootIsPrerendering, |
| 37 | isGestureRender, |
| 38 | } from './ReactFiberLane'; |
| 39 | import { |
| 40 | CommitContext, |
| 41 | NoContext, |
| 42 | RenderContext, |
| 43 | flushPendingEffects, |
| 44 | flushPendingEffectsDelayed, |
| 45 | getExecutionContext, |
| 46 | getWorkInProgressRoot, |
| 47 | getWorkInProgressRootRenderLanes, |
| 48 | getRootWithPendingPassiveEffects, |
| 49 | getPendingPassiveEffectsLanes, |
| 50 | hasPendingCommitEffects, |
| 51 | isWorkLoopSuspendedOnData, |
| 52 | performWorkOnRoot, |
| 53 | } from './ReactFiberWorkLoop'; |
| 54 | import {LegacyRoot} from './ReactRootTags'; |
| 55 | import { |
| 56 | ImmediatePriority as ImmediateSchedulerPriority, |
| 57 | UserBlockingPriority as UserBlockingSchedulerPriority, |
| 58 | NormalPriority as NormalSchedulerPriority, |
| 59 | IdlePriority as IdleSchedulerPriority, |
| 60 | cancelCallback as Scheduler_cancelCallback, |
| 61 | scheduleCallback as Scheduler_scheduleCallback, |
| 62 | now, |
| 63 | } from './Scheduler'; |
| 64 | import { |
| 65 | DiscreteEventPriority, |
| 66 | ContinuousEventPriority, |
| 67 | DefaultEventPriority, |
| 68 | IdleEventPriority, |
| 69 | lanesToEventPriority, |
| 70 | } from './ReactEventPriorities'; |
| 71 | import { |
| 72 | supportsMicrotasks, |
| 73 | scheduleMicrotask, |
| 74 | shouldAttemptEagerTransition, |
| 75 | trackSchedulerEvent, |
| 76 | noTimeout, |
| 77 | } from './ReactFiberConfig'; |
| 78 | |
| 79 | import ReactSharedInternals from 'shared/ReactSharedInternals'; |
| 80 | import { |
| 81 | resetNestedUpdateFlag, |
| 82 | syncNestedUpdateFlag, |
| 83 | } from './ReactProfilerTimer'; |
| 84 | import {peekEntangledActionLane} from './ReactFiberAsyncAction'; |
| 85 | |
| 86 | import noop from 'shared/noop'; |
| 87 | import reportGlobalError from 'shared/reportGlobalError'; |
| 88 | |
| 89 | import { |
| 90 | startIsomorphicDefaultIndicatorIfNeeded, |
| 91 | hasOngoingIsomorphicIndicator, |
| 92 | retainIsomorphicIndicator, |
| 93 | markIsomorphicIndicatorHandled, |
| 94 | } from './ReactFiberAsyncAction'; |
| 95 | |
| 96 | // A linked list of all the roots with pending work. In an idiomatic app, |
| 97 | // there's only a single root, but we do support multi root apps, hence this |
| 98 | // extra complexity. But this module is optimized for the single root case. |
| 99 | export let firstScheduledRoot: FiberRoot | null = null; |
| 100 | let lastScheduledRoot: FiberRoot | null = null; |
| 101 | |
| 102 | // Used to prevent redundant mircotasks from being scheduled. |
| 103 | let didScheduleMicrotask: boolean = false; |
| 104 | // `act` "microtasks" are scheduled on the `act` queue instead of an actual |
| 105 | // microtask, so we have to dedupe those separately. This wouldn't be an issue |
| 106 | // if we required all `act` calls to be awaited, which we might in the future. |
| 107 | let didScheduleMicrotask_act: boolean = false; |
| 108 | |
| 109 | // Used to quickly bail out of flushSync if there's no sync work to do. |
| 110 | let mightHavePendingSyncWork: boolean = false; |
| 111 | |
| 112 | let isFlushingWork: boolean = false; |
| 113 | |
| 114 | let currentEventTransitionLane: Lane = NoLane; |
| 115 | |
| 116 | export function ensureRootIsScheduled(root: FiberRoot): void { |
| 117 | // This function is called whenever a root receives an update. It does two |
| 118 | // things 1) it ensures the root is in the root schedule, and 2) it ensures |
| 119 | // there's a pending microtask to process the root schedule. |
| 120 | // |
| 121 | // Most of the actual scheduling logic does not happen until |
| 122 | // `scheduleTaskForRootDuringMicrotask` runs. |
| 123 | |
| 124 | // Add the root to the schedule |
| 125 | if (root === lastScheduledRoot || root.next !== null) { |
| 126 | // Fast path. This root is already scheduled. |
| 127 | } else { |
| 128 | if (lastScheduledRoot === null) { |
| 129 | firstScheduledRoot = lastScheduledRoot = root; |
| 130 | } else { |
| 131 | lastScheduledRoot.next = root; |
| 132 | lastScheduledRoot = root; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // Any time a root received an update, we set this to true until the next time |
| 137 | // we process the schedule. If it's false, then we can quickly exit flushSync |
| 138 | // without consulting the schedule. |
| 139 | mightHavePendingSyncWork = true; |
| 140 | |
| 141 | ensureScheduleIsScheduled(); |
| 142 | |
| 143 | if ( |
| 144 | __DEV__ && |
| 145 | !disableLegacyMode && |
| 146 | ReactSharedInternals.isBatchingLegacy && |
| 147 | root.tag === LegacyRoot |
| 148 | ) { |
| 149 | // Special `act` case: Record whenever a legacy update is scheduled. |
| 150 | ReactSharedInternals.didScheduleLegacyUpdate = true; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | export function ensureScheduleIsScheduled(): void { |
| 155 | // At the end of the current event, go through each of the roots and ensure |
| 156 | // there's a task scheduled for each one at the correct priority. |
| 157 | if (__DEV__ && ReactSharedInternals.actQueue !== null) { |
| 158 | // We're inside an `act` scope. |
| 159 | if (!didScheduleMicrotask_act) { |
| 160 | didScheduleMicrotask_act = true; |
| 161 | scheduleImmediateRootScheduleTask(); |
| 162 | } |
| 163 | } else { |
| 164 | if (!didScheduleMicrotask) { |
| 165 | didScheduleMicrotask = true; |
| 166 | scheduleImmediateRootScheduleTask(); |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | export function flushSyncWorkOnAllRoots() { |
| 172 | // This is allowed to be called synchronously, but the caller should check |
| 173 | // the execution context first. |
| 174 | flushSyncWorkAcrossRoots_impl(NoLanes, false); |
| 175 | } |
| 176 | |
| 177 | export function flushSyncWorkOnLegacyRootsOnly() { |
| 178 | // This is allowed to be called synchronously, but the caller should check |
| 179 | // the execution context first. |
| 180 | if (!disableLegacyMode) { |
| 181 | flushSyncWorkAcrossRoots_impl(NoLanes, true); |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | function flushSyncWorkAcrossRoots_impl( |
| 186 | syncTransitionLanes: Lanes | Lane, |
| 187 | onlyLegacy: boolean, |
| 188 | ) { |
| 189 | if (isFlushingWork) { |
| 190 | // Prevent reentrancy. |
| 191 | // TODO: Is this overly defensive? The callers must check the execution |
| 192 | // context first regardless. |
| 193 | return; |
| 194 | } |
| 195 | |
| 196 | if (!mightHavePendingSyncWork) { |
| 197 | // Fast path. There's no sync work to do. |
| 198 | return; |
| 199 | } |
| 200 | |
| 201 | // There may or may not be synchronous work scheduled. Let's check. |
| 202 | let didPerformSomeWork; |
| 203 | isFlushingWork = true; |
| 204 | do { |
| 205 | didPerformSomeWork = false; |
| 206 | let root = firstScheduledRoot; |
| 207 | while (root !== null) { |
| 208 | if (onlyLegacy && (disableLegacyMode || root.tag !== LegacyRoot)) { |
| 209 | // Skip non-legacy roots. |
| 210 | } else { |
| 211 | if (syncTransitionLanes !== NoLanes) { |
| 212 | const nextLanes = getNextLanesToFlushSync(root, syncTransitionLanes); |
| 213 | if (nextLanes !== NoLanes) { |
| 214 | // This root has pending sync work. Flush it now. |
| 215 | didPerformSomeWork = true; |
| 216 | performSyncWorkOnRoot(root, nextLanes); |
| 217 | } |
| 218 | } else { |
| 219 | const workInProgressRoot = getWorkInProgressRoot(); |
| 220 | const workInProgressRootRenderLanes = |
| 221 | getWorkInProgressRootRenderLanes(); |
| 222 | const rootHasPendingCommit = |
| 223 | root.cancelPendingCommit !== null || |
| 224 | root.timeoutHandle !== noTimeout; |
| 225 | const nextLanes = getNextLanes( |
| 226 | root, |
| 227 | root === workInProgressRoot |
| 228 | ? workInProgressRootRenderLanes |
| 229 | : NoLanes, |
| 230 | rootHasPendingCommit, |
| 231 | ); |
| 232 | if ( |
| 233 | (includesSyncLane(nextLanes) || |
| 234 | (enableGestureTransition && isGestureRender(nextLanes))) && |
| 235 | !checkIfRootIsPrerendering(root, nextLanes) |
| 236 | ) { |
| 237 | // This root has pending sync work. Flush it now. |
| 238 | didPerformSomeWork = true; |
| 239 | performSyncWorkOnRoot(root, nextLanes); |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | root = root.next; |
| 244 | } |
| 245 | } while (didPerformSomeWork); |
| 246 | isFlushingWork = false; |
| 247 | } |
| 248 | |
| 249 | function processRootScheduleInImmediateTask() { |
| 250 | if (enableProfilerTimer && enableComponentPerformanceTrack) { |
| 251 | // Track the currently executing event if there is one so we can ignore this |
| 252 | // event when logging events. |
| 253 | trackSchedulerEvent(); |
| 254 | } |
| 255 | |
| 256 | processRootScheduleInMicrotask(); |
| 257 | } |
| 258 | |
| 259 | function processRootScheduleInMicrotask() { |
| 260 | // This function is always called inside a microtask. It should never be |
| 261 | // called synchronously. |
| 262 | didScheduleMicrotask = false; |
| 263 | if (__DEV__) { |
| 264 | didScheduleMicrotask_act = false; |
| 265 | } |
| 266 | |
| 267 | // We'll recompute this as we iterate through all the roots and schedule them. |
| 268 | mightHavePendingSyncWork = false; |
| 269 | |
| 270 | let syncTransitionLanes = NoLanes; |
| 271 | if (currentEventTransitionLane !== NoLane) { |
| 272 | if (shouldAttemptEagerTransition()) { |
| 273 | // A transition was scheduled during an event, but we're going to try to |
| 274 | // render it synchronously anyway. We do this during a popstate event to |
| 275 | // preserve the scroll position of the previous page. |
| 276 | syncTransitionLanes = currentEventTransitionLane; |
| 277 | } else if (enableDefaultTransitionIndicator) { |
| 278 | // If we have a Transition scheduled by this event it might be paired |
| 279 | // with Default lane scheduled loading indicators. To unbatch it from |
| 280 | // other events later on, flush it early to determine whether it |
| 281 | // rendered an indicator. This ensures that setState in default priority |
| 282 | // event doesn't trigger onDefaultTransitionIndicator. |
| 283 | syncTransitionLanes = DefaultLane; |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | const currentTime = now(); |
| 288 | |
| 289 | let prev = null; |
| 290 | let root = firstScheduledRoot; |
| 291 | while (root !== null) { |
| 292 | const next = root.next; |
| 293 | const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime); |
| 294 | if (nextLanes === NoLane) { |
| 295 | // This root has no more pending work. Remove it from the schedule. To |
| 296 | // guard against subtle reentrancy bugs, this microtask is the only place |
| 297 | // we do this — you can add roots to the schedule whenever, but you can |
| 298 | // only remove them here. |
| 299 | |
| 300 | // Null this out so we know it's been removed from the schedule. |
| 301 | root.next = null; |
| 302 | if (prev === null) { |
| 303 | // This is the new head of the list |
| 304 | firstScheduledRoot = next; |
| 305 | } else { |
| 306 | prev.next = next; |
| 307 | } |
| 308 | if (next === null) { |
| 309 | // This is the new tail of the list |
| 310 | lastScheduledRoot = prev; |
| 311 | } |
| 312 | } else { |
| 313 | // This root still has work. Keep it in the list. |
| 314 | prev = root; |
| 315 | |
| 316 | // This is a fast-path optimization to early exit from |
| 317 | // flushSyncWorkOnAllRoots if we can be certain that there is no remaining |
| 318 | // synchronous work to perform. Set this to true if there might be sync |
| 319 | // work left. |
| 320 | if ( |
| 321 | // Skip the optimization if syncTransitionLanes is set |
| 322 | syncTransitionLanes !== NoLanes || |
| 323 | // Common case: we're not treating any extra lanes as synchronous, so we |
| 324 | // can just check if the next lanes are sync. |
| 325 | includesSyncLane(nextLanes) || |
| 326 | (enableGestureTransition && isGestureRender(nextLanes)) |
| 327 | ) { |
| 328 | mightHavePendingSyncWork = true; |
| 329 | } |
| 330 | } |
| 331 | root = next; |
| 332 | } |
| 333 | |
| 334 | // At the end of the microtask, flush any pending synchronous work. This has |
| 335 | // to come at the end, because it does actual rendering work that might throw. |
| 336 | // If we're in the middle of a View Transition async sequence, we don't want to |
| 337 | // interrupt that sequence. Instead, we'll flush any remaining work when it |
| 338 | // completes. |
| 339 | if (!hasPendingCommitEffects()) { |
| 340 | flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false); |
| 341 | } |
| 342 | |
| 343 | if (currentEventTransitionLane !== NoLane) { |
| 344 | // Reset Event Transition Lane so that we allocate a new one next time. |
| 345 | currentEventTransitionLane = NoLane; |
| 346 | startDefaultTransitionIndicatorIfNeeded(); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | function startDefaultTransitionIndicatorIfNeeded() { |
| 351 | if (!enableDefaultTransitionIndicator) { |
| 352 | return; |
| 353 | } |
| 354 | // Check if we need to start an isomorphic indicator like if an async action |
| 355 | // was started. |
| 356 | startIsomorphicDefaultIndicatorIfNeeded(); |
| 357 | // Check all the roots if there are any new indicators needed. |
| 358 | let root = firstScheduledRoot; |
| 359 | while (root !== null) { |
| 360 | if (root.indicatorLanes !== NoLanes && root.pendingIndicator === null) { |
| 361 | // We have new indicator lanes that requires a loading state. Start the |
| 362 | // default transition indicator. |
| 363 | if (hasOngoingIsomorphicIndicator()) { |
| 364 | // We already have an isomorphic indicator going which means it has to |
| 365 | // also apply to this root since it implies all roots have the same one. |
| 366 | // We retain this indicator so that it keeps going until we commit this |
| 367 | // root. |
| 368 | root.pendingIndicator = retainIsomorphicIndicator(); |
| 369 | } else { |
| 370 | try { |
| 371 | const onDefaultTransitionIndicator = |
| 372 | root.onDefaultTransitionIndicator; |
| 373 | root.pendingIndicator = onDefaultTransitionIndicator() || noop; |
| 374 | } catch (x) { |
| 375 | root.pendingIndicator = noop; |
| 376 | reportGlobalError(x); |
| 377 | } |
| 378 | } |
| 379 | } |
| 380 | root = root.next; |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | function scheduleTaskForRootDuringMicrotask( |
| 385 | root: FiberRoot, |
| 386 | currentTime: number, |
| 387 | ): Lane { |
| 388 | // This function is always called inside a microtask, or at the very end of a |
| 389 | // rendering task right before we yield to the main thread. It should never be |
| 390 | // called synchronously. |
| 391 | |
| 392 | // This function also never performs React work synchronously; it should |
| 393 | // only schedule work to be performed later, in a separate task or microtask. |
| 394 | |
| 395 | // Check if any lanes are being starved by other work. If so, mark them as |
| 396 | // expired so we know to work on those next. |
| 397 | markStarvedLanesAsExpired(root, currentTime); |
| 398 | |
| 399 | // Determine the next lanes to work on, and their priority. |
| 400 | const rootWithPendingPassiveEffects = getRootWithPendingPassiveEffects(); |
| 401 | const pendingPassiveEffectsLanes = getPendingPassiveEffectsLanes(); |
| 402 | const workInProgressRoot = getWorkInProgressRoot(); |
| 403 | const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); |
| 404 | const rootHasPendingCommit = |
| 405 | root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout; |
| 406 | const nextLanes = |
| 407 | enableYieldingBeforePassive && root === rootWithPendingPassiveEffects |
| 408 | ? // This will schedule the callback at the priority of the lane but we used to |
| 409 | // always schedule it at NormalPriority. Discrete will flush it sync anyway. |
| 410 | // So the only difference is Idle and it doesn't seem necessarily right for that |
| 411 | // to get upgraded beyond something important just because we're past commit. |
| 412 | pendingPassiveEffectsLanes |
| 413 | : getNextLanes( |
| 414 | root, |
| 415 | root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, |
| 416 | rootHasPendingCommit, |
| 417 | ); |
| 418 | |
| 419 | const existingCallbackNode = root.callbackNode; |
| 420 | if ( |
| 421 | // Check if there's nothing to work on |
| 422 | nextLanes === NoLanes || |
| 423 | // If this root is currently suspended and waiting for data to resolve, don't |
| 424 | // schedule a task to render it. We'll either wait for a ping, or wait to |
| 425 | // receive an update. |
| 426 | // |
| 427 | // Suspended render phase |
| 428 | (root === workInProgressRoot && isWorkLoopSuspendedOnData()) || |
| 429 | // Suspended commit phase |
| 430 | root.cancelPendingCommit !== null |
| 431 | ) { |
| 432 | // Fast path: There's nothing to work on. |
| 433 | if (existingCallbackNode !== null) { |
| 434 | cancelCallback(existingCallbackNode); |
| 435 | } |
| 436 | root.callbackNode = null; |
| 437 | root.callbackPriority = NoLane; |
| 438 | return NoLane; |
| 439 | } |
| 440 | |
| 441 | // Schedule a new callback in the host environment. |
| 442 | if ( |
| 443 | includesSyncLane(nextLanes) && |
| 444 | // If we're prerendering, then we should use the concurrent work loop |
| 445 | // even if the lanes are synchronous, so that prerendering never blocks |
| 446 | // the main thread. |
| 447 | !checkIfRootIsPrerendering(root, nextLanes) |
| 448 | ) { |
| 449 | // Synchronous work is always flushed at the end of the microtask, so we |
| 450 | // don't need to schedule an additional task. |
| 451 | if (existingCallbackNode !== null) { |
| 452 | cancelCallback(existingCallbackNode); |
| 453 | } |
| 454 | root.callbackPriority = SyncLane; |
| 455 | root.callbackNode = null; |
| 456 | return SyncLane; |
| 457 | } else { |
| 458 | // We use the highest priority lane to represent the priority of the callback. |
| 459 | const existingCallbackPriority = root.callbackPriority; |
| 460 | const newCallbackPriority = getHighestPriorityLane(nextLanes); |
| 461 | |
| 462 | if ( |
| 463 | newCallbackPriority === existingCallbackPriority && |
| 464 | // Special case related to `act`. If the currently scheduled task is a |
| 465 | // Scheduler task, rather than an `act` task, cancel it and re-schedule |
| 466 | // on the `act` queue. |
| 467 | !( |
| 468 | __DEV__ && |
| 469 | ReactSharedInternals.actQueue !== null && |
| 470 | existingCallbackNode !== fakeActCallbackNode |
| 471 | ) |
| 472 | ) { |
| 473 | // The priority hasn't changed. We can reuse the existing task. |
| 474 | return newCallbackPriority; |
| 475 | } else { |
| 476 | // Cancel the existing callback. We'll schedule a new one below. |
| 477 | cancelCallback(existingCallbackNode); |
| 478 | } |
| 479 | |
| 480 | let schedulerPriorityLevel; |
| 481 | switch (lanesToEventPriority(nextLanes)) { |
| 482 | // Scheduler does have an "ImmediatePriority", but now that we use |
| 483 | // microtasks for sync work we no longer use that. Any sync work that |
| 484 | // reaches this path is meant to be time sliced. |
| 485 | case DiscreteEventPriority: |
| 486 | case ContinuousEventPriority: |
| 487 | schedulerPriorityLevel = UserBlockingSchedulerPriority; |
| 488 | break; |
| 489 | case DefaultEventPriority: |
| 490 | schedulerPriorityLevel = NormalSchedulerPriority; |
| 491 | break; |
| 492 | case IdleEventPriority: |
| 493 | schedulerPriorityLevel = IdleSchedulerPriority; |
| 494 | break; |
| 495 | default: |
| 496 | schedulerPriorityLevel = NormalSchedulerPriority; |
| 497 | break; |
| 498 | } |
| 499 | |
| 500 | const newCallbackNode = scheduleCallback( |
| 501 | schedulerPriorityLevel, |
| 502 | performWorkOnRootViaSchedulerTask.bind(null, root), |
| 503 | ); |
| 504 | |
| 505 | root.callbackPriority = newCallbackPriority; |
| 506 | root.callbackNode = newCallbackNode; |
| 507 | return newCallbackPriority; |
| 508 | } |
| 509 | } |
| 510 | |
| 511 | type RenderTaskFn = (didTimeout: boolean) => RenderTaskFn | null; |
| 512 | |
| 513 | function performWorkOnRootViaSchedulerTask( |
| 514 | root: FiberRoot, |
| 515 | didTimeout: boolean, |
| 516 | ): RenderTaskFn | null { |
| 517 | // This is the entry point for concurrent tasks scheduled via Scheduler (and |
| 518 | // postTask, in the future). |
| 519 | |
| 520 | if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { |
| 521 | resetNestedUpdateFlag(); |
| 522 | } |
| 523 | |
| 524 | if (enableProfilerTimer && enableComponentPerformanceTrack) { |
| 525 | // Track the currently executing event if there is one so we can ignore this |
| 526 | // event when logging events. |
| 527 | trackSchedulerEvent(); |
| 528 | } |
| 529 | |
| 530 | if (hasPendingCommitEffects()) { |
| 531 | // We are currently in the middle of an async committing (such as a View Transition). |
| 532 | // We could force these to flush eagerly but it's better to defer any work until |
| 533 | // it finishes. This may not be the same root as we're waiting on. |
| 534 | // TODO: This relies on the commit eventually calling ensureRootIsScheduled which |
| 535 | // always calls processRootScheduleInMicrotask which in turn always loops through |
| 536 | // all the roots to figure out. This is all a bit inefficient and if optimized |
| 537 | // it'll need to consider rescheduling a task for any skipped roots. |
| 538 | root.callbackNode = null; |
| 539 | root.callbackPriority = NoLane; |
| 540 | return null; |
| 541 | } |
| 542 | |
| 543 | // Flush any pending passive effects before deciding which lanes to work on, |
| 544 | // in case they schedule additional work. |
| 545 | const originalCallbackNode = root.callbackNode; |
| 546 | const didFlushPassiveEffects = flushPendingEffectsDelayed(); |
| 547 | if (didFlushPassiveEffects) { |
| 548 | // Something in the passive effect phase may have canceled the current task. |
| 549 | // Check if the task node for this root was changed. |
| 550 | if (root.callbackNode !== originalCallbackNode) { |
| 551 | // The current task was canceled. Exit. We don't need to call |
| 552 | // `ensureRootIsScheduled` because the check above implies either that |
| 553 | // there's a new task, or that there's no remaining work on this root. |
| 554 | return null; |
| 555 | } else { |
| 556 | // Current task was not canceled. Continue. |
| 557 | } |
| 558 | } |
| 559 | |
| 560 | // Determine the next lanes to work on, using the fields stored on the root. |
| 561 | // TODO: We already called getNextLanes when we scheduled the callback; we |
| 562 | // should be able to avoid calling it again by stashing the result on the |
| 563 | // root object. However, because we always schedule the callback during |
| 564 | // a microtask (scheduleTaskForRootDuringMicrotask), it's possible that |
| 565 | // an update was scheduled earlier during this same browser task (and |
| 566 | // therefore before the microtasks have run). That's because Scheduler batches |
| 567 | // together multiple callbacks into a single browser macrotask, without |
| 568 | // yielding to microtasks in between. We should probably change this to align |
| 569 | // with the postTask behavior (and literally use postTask when |
| 570 | // it's available). |
| 571 | const workInProgressRoot = getWorkInProgressRoot(); |
| 572 | const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes(); |
| 573 | const rootHasPendingCommit = |
| 574 | root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout; |
| 575 | const lanes = getNextLanes( |
| 576 | root, |
| 577 | root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes, |
| 578 | rootHasPendingCommit, |
| 579 | ); |
| 580 | if (lanes === NoLanes) { |
| 581 | // No more work on this root. |
| 582 | return null; |
| 583 | } |
| 584 | |
| 585 | // Enter the work loop. |
| 586 | // TODO: We only check `didTimeout` defensively, to account for a Scheduler |
| 587 | // bug we're still investigating. Once the bug in Scheduler is fixed, |
| 588 | // we can remove this, since we track expiration ourselves. |
| 589 | const forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout; |
| 590 | performWorkOnRoot(root, lanes, forceSync); |
| 591 | |
| 592 | // The work loop yielded, but there may or may not be work left at the current |
| 593 | // priority. Need to determine whether we need to schedule a continuation. |
| 594 | // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask; |
| 595 | // however, since most of the logic for determining if we need a continuation |
| 596 | // versus a new task is the same, we cheat a bit and call it here. This is |
| 597 | // only safe to do because we know we're at the end of the browser task. |
| 598 | // So although it's not an actual microtask, it might as well be. |
| 599 | scheduleTaskForRootDuringMicrotask(root, now()); |
| 600 | if (root.callbackNode != null && root.callbackNode === originalCallbackNode) { |
| 601 | // The task node scheduled for this root is the same one that's |
| 602 | // currently executed. Need to return a continuation. |
| 603 | return performWorkOnRootViaSchedulerTask.bind(null, root); |
| 604 | } |
| 605 | return null; |
| 606 | } |
| 607 | |
| 608 | function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) { |
| 609 | // This is the entry point for synchronous tasks that don't go |
| 610 | // through Scheduler. |
| 611 | const didFlushPassiveEffects = flushPendingEffects(); |
| 612 | if (didFlushPassiveEffects) { |
| 613 | // If passive effects were flushed, exit to the outer work loop in the root |
| 614 | // scheduler, so we can recompute the priority. |
| 615 | return null; |
| 616 | } |
| 617 | if (enableProfilerTimer && enableProfilerNestedUpdatePhase) { |
| 618 | syncNestedUpdateFlag(); |
| 619 | } |
| 620 | const forceSync = true; |
| 621 | performWorkOnRoot(root, lanes, forceSync); |
| 622 | } |
| 623 | |
| 624 | const fakeActCallbackNode = {}; |
| 625 | |
| 626 | function scheduleCallback( |
| 627 | priorityLevel: PriorityLevel, |
| 628 | callback: RenderTaskFn, |
| 629 | ) { |
| 630 | if (__DEV__ && ReactSharedInternals.actQueue !== null) { |
| 631 | // Special case: We're inside an `act` scope (a testing utility). |
| 632 | // Instead of scheduling work in the host environment, add it to a |
| 633 | // fake internal queue that's managed by the `act` implementation. |
| 634 | ReactSharedInternals.actQueue.push(callback); |
| 635 | return fakeActCallbackNode; |
| 636 | } else { |
| 637 | return Scheduler_scheduleCallback(priorityLevel, callback); |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | function cancelCallback(callbackNode: mixed) { |
| 642 | if (__DEV__ && callbackNode === fakeActCallbackNode) { |
| 643 | // Special `act` case: check if this is the fake callback node used by |
| 644 | // the `act` implementation. |
| 645 | } else if (callbackNode !== null) { |
| 646 | Scheduler_cancelCallback(callbackNode); |
| 647 | } |
| 648 | } |
| 649 | |
| 650 | function scheduleImmediateRootScheduleTask() { |
| 651 | if (__DEV__ && ReactSharedInternals.actQueue !== null) { |
| 652 | // Special case: Inside an `act` scope, we push microtasks to the fake `act` |
| 653 | // callback queue. This is because we currently support calling `act` |
| 654 | // without awaiting the result. The plan is to deprecate that, and require |
| 655 | // that you always await the result so that the microtasks have a chance to |
| 656 | // run. But it hasn't happened yet. |
| 657 | ReactSharedInternals.actQueue.push(() => { |
| 658 | processRootScheduleInMicrotask(); |
| 659 | return null; |
| 660 | }); |
| 661 | } |
| 662 | |
| 663 | // TODO: Can we land supportsMicrotasks? Which environments don't support it? |
| 664 | // Alternatively, can we move this check to the host config? |
| 665 | // $FlowFixMe[constant-condition] |
| 666 | if (supportsMicrotasks) { |
| 667 | scheduleMicrotask(() => { |
| 668 | // In Safari, appending an iframe forces microtasks to run. |
| 669 | // https://github.com/facebook/react/issues/22459 |
| 670 | // We don't support running callbacks in the middle of render |
| 671 | // or commit so we need to check against that. |
| 672 | const executionContext = getExecutionContext(); |
| 673 | if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { |
| 674 | // Note that this would still prematurely flush the callbacks |
| 675 | // if this happens outside render or commit phase (e.g. in an event). |
| 676 | |
| 677 | // Intentionally using a macrotask instead of a microtask here. This is |
| 678 | // wrong semantically but it prevents an infinite loop. The bug is |
| 679 | // Safari's, not ours, so we just do our best to not crash even though |
| 680 | // the behavior isn't completely correct. |
| 681 | Scheduler_scheduleCallback( |
| 682 | ImmediateSchedulerPriority, |
| 683 | processRootScheduleInImmediateTask, |
| 684 | ); |
| 685 | return; |
| 686 | } |
| 687 | processRootScheduleInMicrotask(); |
| 688 | }); |
| 689 | } else { |
| 690 | // If microtasks are not supported, use Scheduler. |
| 691 | Scheduler_scheduleCallback( |
| 692 | ImmediateSchedulerPriority, |
| 693 | processRootScheduleInImmediateTask, |
| 694 | ); |
| 695 | } |
| 696 | } |
| 697 | |
| 698 | export function requestTransitionLane( |
| 699 | // This argument isn't used, it's only here to encourage the caller to |
| 700 | // check that it's inside a transition before calling this function. |
| 701 | // TODO: Make this non-nullable. Requires a tweak to useOptimistic. |
| 702 | transition: Transition | null, |
| 703 | ): Lane { |
| 704 | // The algorithm for assigning an update to a lane should be stable for all |
| 705 | // updates at the same priority within the same event. To do this, the |
| 706 | // inputs to the algorithm must be the same. |
| 707 | // |
| 708 | // The trick we use is to cache the first of each of these inputs within an |
| 709 | // event. Then reset the cached values once we can be sure the event is |
| 710 | // over. Our heuristic for that is whenever we enter a concurrent work loop. |
| 711 | if (currentEventTransitionLane === NoLane) { |
| 712 | // All transitions within the same event are assigned the same lane. |
| 713 | const actionScopeLane = peekEntangledActionLane(); |
| 714 | currentEventTransitionLane = |
| 715 | actionScopeLane !== NoLane |
| 716 | ? // We're inside an async action scope. Reuse the same lane. |
| 717 | actionScopeLane |
| 718 | : // We may or may not be inside an async action scope. If we are, this |
| 719 | // is the first update in that scope. Either way, we need to get a |
| 720 | // fresh transition lane. |
| 721 | claimNextTransitionUpdateLane(); |
| 722 | } |
| 723 | return currentEventTransitionLane; |
| 724 | } |
| 725 | |
| 726 | export function didCurrentEventScheduleTransition(): boolean { |
| 727 | return currentEventTransitionLane !== NoLane; |
| 728 | } |
| 729 | |
| 730 | export function markIndicatorHandled(root: FiberRoot): void { |
| 731 | if (enableDefaultTransitionIndicator) { |
| 732 | // The current transition event rendered a synchronous loading state. |
| 733 | // Clear it from the indicator lanes. We don't need to show a separate |
| 734 | // loading state for this lane. |
| 735 | root.indicatorLanes &= ~currentEventTransitionLane; |
| 736 | markIsomorphicIndicatorHandled(); |
| 737 | } |
| 738 | } |