10
import type {Fiber} from './ReactFiber';
11
import type {FiberRoot} from './ReactFiberRoot';
12
import type {ExpirationTime} from './ReactFiberExpirationTime';
13
-import type {
14
- ReactPriorityLevel,
15
- SchedulerCallback,
16
-} from './SchedulerWithReactIntegration';
13
+import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
14
import type {Interaction} from 'scheduler/src/Tracing';
15
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16
import type {SuspenseState} from './ReactFiberSuspenseComponent';
63
} from './ReactFiberHostConfig';
64
65
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
66
+import {
67
+ isRootSuspendedAtTime,
68
+ markRootSuspendedAtTime,
69
+ markRootFinishedAtTime,
70
+ markRootUpdatedAtTime,
71
+ markRootExpiredAtTime,
72
+} from './ReactFiberRoot';
73
import {
74
NoMode,
75
StrictMode,
116
inferPriorityFromExpirationTime,
117
LOW_PRIORITY_EXPIRATION,
118
Batched,
119
+ Idle,
120
} from './ReactFiberExpirationTime';
121
import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
122
import {completeWork} from './ReactFiberCompleteWork';
228
let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
229
let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
230
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
231
+// The work left over by components that were visited during this render. Only
232
+// includes unprocessed updates, not work in bailed out children.
233
+let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
234
+
235
// If we're pinged while rendering we don't always restart immediately.
236
// This flag determines if it might be worthwhile to restart if an opportunity
237
// happens latere.
387
return;
388
}
389
381
- root.pingTime = NoWork;
382
-
390
checkForInterruption(fiber, expirationTime);
391
recordScheduleUpdate();
392
407
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
408
// root inside of batchedUpdates should be synchronous, but layout updates
409
// should be deferred until the end of the batch.
403
- let callback = renderRoot(root, Sync, true);
404
- while (callback !== null) {
405
- callback = callback(true);
406
- }
410
+ performSyncWorkOnRoot(root);
411
} else {
408
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
412
+ ensureRootIsScheduled(root);
413
+ schedulePendingInteractions(root, expirationTime);
414
if (executionContext === NoContext) {
415
// Flush the synchronous work now, unless we're already working or inside
416
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
421
}
422
}
423
} else {
419
- scheduleCallbackForRoot(root, priorityLevel, expirationTime);
424
+ ensureRootIsScheduled(root);
425
+ schedulePendingInteractions(root, expirationTime);
426
}
427
428
if (
490
}
491
492
if (root !== null) {
487
- // Update the first and last pending expiration times in this root
488
- const firstPendingTime = root.firstPendingTime;
489
- if (expirationTime > firstPendingTime) {
490
- root.firstPendingTime = expirationTime;
491
- }
492
- const lastPendingTime = root.lastPendingTime;
493
- if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
494
- root.lastPendingTime = expirationTime;
493
+ if (workInProgressRoot === root) {
494
+ // Received an update to a tree that's in the middle of rendering. Mark
495
+ // that's unprocessed work on this root.
496
+ markUnprocessedUpdateTime(expirationTime);
497
+
498
+ if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
499
+ // The root already suspended with a delay, which means this render
500
+ // definitely won't finish. Since we have a new update, let's mark it as
501
+ // suspended now, right before marking the incoming update. This has the
502
+ // effect of interrupting the current render and switching to the update.
503
+ // TODO: This happens to work when receiving an update during the render
504
+ // phase, because of the trick inside computeExpirationForFiber to
505
+ // subtract 1 from `renderExpirationTime` to move it into a
506
+ // separate bucket. But we should probably model it with an exception,
507
+ // using the same mechanism we use to force hydration of a subtree.
508
+ // TODO: This does not account for low pri updates that were already
509
+ // scheduled before the root started rendering. Need to track the next
510
+ // pending expiration time (perhaps by backtracking the return path) and
511
+ // then trigger a restart in the `renderDidSuspendDelayIfPossible` path.
512
+ markRootSuspendedAtTime(root, renderExpirationTime);
513
+ }
514
}
515
+ // Mark that the root has a pending update.
516
+ markRootUpdatedAtTime(root, expirationTime);
517
}
518
519
return root;
520
}
521
501
-// Use this function, along with runRootCallback, to ensure that only a single
502
-// callback per root is scheduled. It's still possible to call renderRoot
503
-// directly, but scheduling via this function helps avoid excessive callbacks.
504
-// It works by storing the callback node and expiration time on the root. When a
505
-// new callback comes in, it compares the expiration time to determine if it
506
-// should cancel the previous one. It also relies on commitRoot scheduling a
507
-// callback to render the next level, because that means we don't need a
508
-// separate callback per expiration time.
509
-function scheduleCallbackForRoot(
510
- root: FiberRoot,
511
- priorityLevel: ReactPriorityLevel,
512
- expirationTime: ExpirationTime,
513
-) {
514
- const existingCallbackExpirationTime = root.callbackExpirationTime;
515
- if (existingCallbackExpirationTime < expirationTime) {
516
- // New callback has higher priority than the existing one.
517
- const existingCallbackNode = root.callbackNode;
522
+function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
523
+ // Determines the next expiration time that the root should render, taking
524
+ // into account levels that may be suspended, or levels that may have
525
+ // received a ping.
526
+
527
+ const lastExpiredTime = root.lastExpiredTime;
528
+ if (lastExpiredTime !== NoWork) {
529
+ return lastExpiredTime;
530
+ }
531
+
532
+ // "Pending" refers to any update that hasn't committed yet, including if it
533
+ // suspended. The "suspended" range is therefore a subset.
534
+ const firstPendingTime = root.firstPendingTime;
535
+ if (!isRootSuspendedAtTime(root, firstPendingTime)) {
536
+ // The highest priority pending time is not suspended. Let's work on that.
537
+ return firstPendingTime;
538
+ }
539
+
540
+ // If the first pending time is suspended, check if there's a lower priority
541
+ // pending level that we know about. Or check if we received a ping. Work
542
+ // on whichever is higher priority.
543
+ const lastPingedTime = root.lastPingedTime;
544
+ const nextKnownPendingLevel = root.nextKnownPendingLevel;
545
+ return lastPingedTime > nextKnownPendingLevel
546
+ ? lastPingedTime
547
+ : nextKnownPendingLevel;
548
+}
549
+
550
+// Use this function to schedule a task for a root. There's only one task per
551
+// root; if a task was already scheduled, we'll check to make sure the
552
+// expiration time of the existing task is the same as the expiration time of
553
+// the next level that the root has work on. This function is called on every
554
+// update, and right before exiting a task.
555
+function ensureRootIsScheduled(root: FiberRoot) {
556
+ const lastExpiredTime = root.lastExpiredTime;
557
+ if (lastExpiredTime !== NoWork) {
558
+ // Special case: Expired work should flush synchronously.
559
+ root.callbackExpirationTime = Sync;
560
+ root.callbackPriority = ImmediatePriority;
561
+ root.callbackNode = scheduleSyncCallback(
562
+ performSyncWorkOnRoot.bind(null, root),
563
+ );
564
+ return;
565
+ }
566
+
567
+ const expirationTime = getNextRootExpirationTimeToWorkOn(root);
568
+ const existingCallbackNode = root.callbackNode;
569
+ if (expirationTime === NoWork) {
570
+ // There's nothing to work on.
571
if (existingCallbackNode !== null) {
519
- cancelCallback(existingCallbackNode);
572
+ root.callbackNode = null;
573
+ root.callbackExpirationTime = NoWork;
574
+ root.callbackPriority = NoPriority;
575
}
521
- root.callbackExpirationTime = expirationTime;
576
+ return;
577
+ }
578
523
- if (expirationTime === Sync) {
524
- // Sync React callbacks are scheduled on a special internal queue
525
- root.callbackNode = scheduleSyncCallback(
526
- runRootCallback.bind(
527
- null,
528
- root,
529
- renderRoot.bind(null, root, expirationTime),
530
- ),
531
- );
532
- } else {
533
- let options = null;
534
- if (
535
- !disableSchedulerTimeoutBasedOnReactExpirationTime &&
536
- expirationTime !== Never
537
- ) {
538
- let timeout = expirationTimeToMs(expirationTime) - now();
539
- options = {timeout};
540
- }
579
+ // TODO: If this is an update, we already read the current time. Pass the
580
+ // time as an argument.
581
+ const currentTime = requestCurrentTime();
582
+ const priorityLevel = inferPriorityFromExpirationTime(
583
+ currentTime,
584
+ expirationTime,
585
+ );
586
542
- root.callbackNode = scheduleCallback(
543
- priorityLevel,
544
- runRootCallback.bind(
545
- null,
546
- root,
547
- renderRoot.bind(null, root, expirationTime),
548
- ),
549
- options,
550
- );
587
+ // If there's an existing render task, confirm it has the correct priority and
588
+ // expiration time. Otherwise, we'll cancel it and schedule a new one.
589
+ if (existingCallbackNode !== null) {
590
+ const existingCallbackPriority = root.callbackPriority;
591
+ const existingCallbackExpirationTime = root.callbackExpirationTime;
592
+ if (
593
+ // Callback must have the exact same expiration time.
594
+ existingCallbackExpirationTime === expirationTime &&
595
+ // Callback must have greater or equal priority.
596
+ existingCallbackPriority >= priorityLevel
597
+ ) {
598
+ // Existing callback is sufficient.
599
+ return;
600
}
601
+ // Need to schedule a new task.
602
+ // TODO: Instead of scheduling a new task, we should be able to change the
603
+ // priority of the existing one.
604
+ cancelCallback(existingCallbackNode);
605
+ }
606
+
607
+ root.callbackExpirationTime = expirationTime;
608
+ root.callbackPriority = priorityLevel;
609
+
610
+ let callbackNode;
611
+ if (expirationTime === Sync) {
612
+ // Sync React callbacks are scheduled on a special internal queue
613
+ callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
614
+ } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
615
+ callbackNode = scheduleCallback(
616
+ priorityLevel,
617
+ performConcurrentWorkOnRoot.bind(null, root),
618
+ );
619
+ } else {
620
+ callbackNode = scheduleCallback(
621
+ priorityLevel,
622
+ performConcurrentWorkOnRoot.bind(null, root),
623
+ // Compute a task timeout based on the expiration time. This also affects
624
+ // ordering because tasks are processed in timeout order.
625
+ {timeout: expirationTimeToMs(expirationTime) - now()},
626
+ );
627
}
628
554
- // Associate the current interactions with this new root+priority.
555
- schedulePendingInteractions(root, expirationTime);
629
+ root.callbackNode = callbackNode;
630
}
631
558
-function runRootCallback(root, callback, isSync) {
559
- const prevCallbackNode = root.callbackNode;
560
- let continuation = null;
561
- try {
562
- continuation = callback(isSync);
563
- if (continuation !== null) {
564
- return runRootCallback.bind(null, root, continuation);
565
- } else {
566
- return null;
632
+// This is the entry point for every concurrent task, i.e. anything that
633
+// goes through Scheduler.
634
+function performConcurrentWorkOnRoot(root, didTimeout) {
635
+ // Since we know we're in a React event, we can clear the current
636
+ // event time. The next update will compute a new event time.
637
+ currentEventTime = NoWork;
638
+
639
+ if (didTimeout) {
640
+ // An async update expired.
641
+ const currentTime = requestCurrentTime();
642
+ markRootExpiredAtTime(root, currentTime);
643
+ }
644
+
645
+ // Determine the next expiration time to work on, using the fields stored
646
+ // on the root.
647
+ const expirationTime = getNextRootExpirationTimeToWorkOn(root);
648
+ if (expirationTime !== NoWork) {
649
+ const originalCallbackNode = root.callbackNode;
650
+ try {
651
+ renderRoot(root, expirationTime, didTimeout);
652
+ if (root.callbackNode === originalCallbackNode) {
653
+ // The task node scheduled for this root is the same one that's
654
+ // currently executed. Need to return a continuation.
655
+ return performConcurrentWorkOnRoot.bind(null, root);
656
+ }
657
+ } finally {
658
+ // Before exiting, make sure there's a callback scheduled for the
659
+ // pending level.
660
+ ensureRootIsScheduled(root);
661
}
662
+ }
663
+ return null;
664
+}
665
+
666
+// This is the entry point for synchronous tasks that don't go
667
+// through Scheduler
668
+function performSyncWorkOnRoot(root) {
669
+ // Check if there's expired work on this root. Otherwise, render at Sync.
670
+ const lastExpiredTime = root.lastExpiredTime;
671
+ const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
672
+ try {
673
+ renderRoot(root, expirationTime, true);
674
} finally {
569
- // If the callback exits without returning a continuation, remove the
570
- // corresponding callback node from the root. Unless the callback node
571
- // has changed, which implies that it was already cancelled by a high
572
- // priority update.
573
- if (continuation === null && prevCallbackNode === root.callbackNode) {
574
- root.callbackNode = null;
575
- root.callbackExpirationTime = NoWork;
576
- }
675
+ // Before exiting, make sure there's a callback scheduled for the
676
+ // pending level.
677
+ ensureRootIsScheduled(root);
678
}
679
+ return null;
680
}
681
682
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
687
'means you attempted to commit from inside a lifecycle method.',
688
);
689
}
588
- scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
690
+ markRootExpiredAtTime(root, expirationTime);
691
+ ensureRootIsScheduled(root);
692
flushSyncCallbackQueue();
693
}
694
757
const roots = rootsWithPendingDiscreteUpdates;
758
rootsWithPendingDiscreteUpdates = null;
759
roots.forEach((expirationTime, root) => {
657
- scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
760
+ markRootExpiredAtTime(root, expirationTime);
761
+ ensureRootIsScheduled(root);
762
});
763
// Now flush the immediate queue.
764
flushSyncCallbackQueue();
890
workInProgressRootLatestProcessedExpirationTime = Sync;
891
workInProgressRootLatestSuspenseTimeout = Sync;
892
workInProgressRootCanSuspendUsingConfig = null;
893
+ workInProgressRootNextUnprocessedUpdateTime = NoWork;
894
workInProgressRootHasPendingPing = false;
895
896
if (enableSchedulerTracing) {
903
}
904
}
905
906
+// renderRoot should only be called from inside either
907
+// `performConcurrentWorkOnRoot` or `performSyncWorkOnRoot`.
908
function renderRoot(
909
root: FiberRoot,
910
expirationTime: ExpirationTime,
911
isSync: boolean,
805
-): SchedulerCallback | null {
912
+): void {
913
invariant(
914
(executionContext & (RenderContext | CommitContext)) === NoContext,
915
'Should not already be working.',
916
);
917
811
- if (root.firstPendingTime < expirationTime) {
812
- // If there's no work left at this expiration time, exit immediately. This
813
- // happens when multiple callbacks are scheduled for a single root, but an
814
- // earlier callback flushes the work of a later one.
815
- return null;
816
- }
817
-
918
if (isSync && root.finishedExpirationTime === expirationTime) {
919
// There's already a pending commit at this expiration time.
920
// TODO: This is poorly factored. This case only exists for the
921
// batch.commit() API.
822
- return commitRoot.bind(null, root);
922
+ commitRoot(root);
923
+ return;
924
}
925
926
flushPassiveEffects();
930
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
931
prepareFreshStack(root, expirationTime);
932
startWorkOnPendingInteractions(root, expirationTime);
832
- } else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
833
- // We could've received an update at a lower priority while we yielded.
834
- // We're suspended in a delayed state. Once we complete this render we're
835
- // just going to try to recover at the last pending time anyway so we might
836
- // as well start doing that eagerly.
837
- // Ideally we should be able to do this even for retries but we don't yet
838
- // know if we're going to process an update which wants to commit earlier,
839
- // and this path happens very early so it would happen too often. Instead,
840
- // for that case, we'll wait until we complete.
841
- if (workInProgressRootHasPendingPing) {
842
- // We have a ping at this expiration. Let's restart to see if we get unblocked.
843
- prepareFreshStack(root, expirationTime);
844
- } else {
845
- const lastPendingTime = root.lastPendingTime;
846
- if (lastPendingTime < expirationTime) {
847
- // There's lower priority work. It might be unsuspended. Try rendering
848
- // at that level immediately, while preserving the position in the queue.
849
- return renderRoot.bind(null, root, lastPendingTime);
850
- }
851
- }
933
}
934
935
// If we have a work-in-progress fiber, it means there's still work to do
953
954
startWorkLoopTimer(workInProgress);
955
875
- // TODO: Fork renderRoot into renderRootSync and renderRootAsync
876
- if (isSync) {
877
- if (expirationTime !== Sync) {
878
- // An async update expired. There may be other expired updates on
879
- // this root. We should render all the expired work in a
880
- // single batch.
881
- const currentTime = requestCurrentTime();
882
- if (currentTime < expirationTime) {
883
- // Restart at the current time.
884
- executionContext = prevExecutionContext;
885
- resetContextDependencies();
886
- ReactCurrentDispatcher.current = prevDispatcher;
887
- if (enableSchedulerTracing) {
888
- __interactionsRef.current = ((prevInteractions: any): Set<
889
- Interaction,
890
- >);
891
- }
892
- return renderRoot.bind(null, root, currentTime);
893
- }
894
- }
895
- } else {
896
- // Since we know we're in a React event, we can clear the current
897
- // event time. The next update will compute a new event time.
898
- currentEventTime = NoWork;
899
- }
900
-
956
do {
957
try {
958
if (isSync) {
974
// boundary.
975
prepareFreshStack(root, expirationTime);
976
executionContext = prevExecutionContext;
977
+ markRootSuspendedAtTime(root, expirationTime);
978
throw thrownValue;
979
}
980
993
thrownValue,
994
renderExpirationTime,
995
);
996
+ // TODO: This is not wrapped in a try-catch, so if the complete phase
997
+ // throws, we won't capture it.
998
workInProgress = completeUnitOfWork(sourceFiber);
999
}
1000
} while (true);
1007
}
1008
1009
if (workInProgress !== null) {
952
- // There's still work left over. Return a continuation.
1010
+ // There's still work left over. Exit without committing.
1011
stopInterruptedWorkLoopTimer();
954
- return renderRoot.bind(null, root, expirationTime);
1012
+ return;
1013
}
1014
}
1015
1017
// something suspended, wait to commit it after a timeout.
1018
stopFinishedWorkLoopTimer();
1019
962
- root.finishedWork = root.current.alternate;
1020
+ const finishedWork: Fiber = ((root.finishedWork =
1021
+ root.current.alternate): any);
1022
root.finishedExpirationTime = expirationTime;
1023
1024
const isLocked = resolveLocksOnRoot(root, expirationTime);
1026
// This root has a lock that prevents it from committing. Exit. If we begin
1027
// work on the root again, without any intervening updates, it will finish
1028
// without doing additional work.
970
- return null;
1029
+ markRootSuspendedAtTime(root, expirationTime);
1030
+ return;
1031
}
1032
1033
// Set this to null to indicate there's no in-progress render.
1041
// but eslint doesn't know about invariant, so it complains if I do.
1042
// eslint-disable-next-line no-fallthrough
1043
case RootErrored: {
984
- // An error was thrown. First check if there is lower priority work
985
- // scheduled on this root.
986
- const lastPendingTime = root.lastPendingTime;
987
- if (lastPendingTime < expirationTime) {
988
- // There's lower priority work. Before raising the error, try rendering
989
- // at the lower priority to see if it fixes it. Use a continuation to
990
- // maintain the existing priority and position in the queue.
991
- return renderRoot.bind(null, root, lastPendingTime);
992
- }
993
- if (!isSync) {
994
- // If we're rendering asynchronously, it's possible the error was
995
- // caused by tearing due to a mutation during an event. Try rendering
996
- // one more time without yiedling to events.
997
- prepareFreshStack(root, expirationTime);
998
- scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
999
- return null;
1044
+ if (!isSync && expirationTime !== Idle) {
1045
+ // If this was an async render, the error may have happened due to
1046
+ // a mutation in a concurrent event. Try rendering one more time,
1047
+ // synchronously, to see if the error goes away. If there are lower
1048
+ // priority updates, let's include those, too, in case they fix the
1049
+ // inconsistency. Render at Idle to include all updates.
1050
+ markRootExpiredAtTime(root, Idle);
1051
+ return;
1052
}
1001
- // If we're already rendering synchronously, commit the root in its
1002
- // errored state.
1003
- return commitRoot.bind(null, root);
1053
+ // Commit the root in its errored state.
1054
+ commitRoot(root);
1055
+ return;
1056
}
1057
case RootSuspended: {
1058
+ markRootSuspendedAtTime(root, expirationTime);
1059
+ const lastSuspendedTime = root.lastSuspendedTime;
1060
+ if (expirationTime === lastSuspendedTime) {
1061
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1062
+ }
1063
flushSuspensePriorityWarningInDEV();
1064
1065
// We have an acceptable loading state. We need to figure out if we should
1091
// Don't bother with a very short suspense time.
1092
if (msUntilTimeout > 10) {
1093
if (workInProgressRootHasPendingPing) {
1037
- // This render was pinged but we didn't get to restart earlier so try
1038
- // restarting now instead.
1039
- prepareFreshStack(root, expirationTime);
1040
- return renderRoot.bind(null, root, expirationTime);
1094
+ const lastPingedTime = root.lastPingedTime;
1095
+ if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
1096
+ // This render was pinged but we didn't get to restart earlier so
1097
+ // try restarting now instead.
1098
+ root.lastPingedTime = expirationTime;
1099
+ prepareFreshStack(root, expirationTime);
1100
+ return;
1101
+ }
1102
}
1042
- const lastPendingTime = root.lastPendingTime;
1043
- if (lastPendingTime < expirationTime) {
1044
- // There's lower priority work. It might be unsuspended. Try rendering
1045
- // at that level.
1046
- return renderRoot.bind(null, root, lastPendingTime);
1103
+
1104
+ const nextTime = getNextRootExpirationTimeToWorkOn(root);
1105
+ if (nextTime !== NoWork && nextTime !== expirationTime) {
1106
+ // There's additional work on this root.
1107
+ return;
1108
}
1109
+ if (
1110
+ lastSuspendedTime !== NoWork &&
1111
+ lastSuspendedTime !== expirationTime
1112
+ ) {
1113
+ // We should prefer to render the fallback of at the last suspended
1114
+ // level. Ping the last suspended level to try rendering it again.
1115
+ root.lastPingedTime = lastSuspendedTime;
1116
+ return;
1117
+ }
1118
+
1119
// The render is suspended, it hasn't timed out, and there's no lower
1120
// priority work to do. Instead of committing the fallback
1121
// immediately, wait for more data to arrive.
1123
commitRoot.bind(null, root),
1124
msUntilTimeout,
1125
);
1055
- return null;
1126
+ return;
1127
}
1128
}
1129
// The work expired. Commit immediately.
1059
- return commitRoot.bind(null, root);
1130
+ commitRoot(root);
1131
+ return;
1132
}
1133
case RootSuspendedWithDelay: {
1134
+ markRootSuspendedAtTime(root, expirationTime);
1135
+ const lastSuspendedTime = root.lastSuspendedTime;
1136
+ if (expirationTime === lastSuspendedTime) {
1137
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1138
+ }
1139
flushSuspensePriorityWarningInDEV();
1140
1141
if (
1150
// We're suspended in a state that should be avoided. We'll try to avoid committing
1151
// it for as long as the timeouts let us.
1152
if (workInProgressRootHasPendingPing) {
1076
- // This render was pinged but we didn't get to restart earlier so try
1077
- // restarting now instead.
1078
- prepareFreshStack(root, expirationTime);
1079
- return renderRoot.bind(null, root, expirationTime);
1153
+ const lastPingedTime = root.lastPingedTime;
1154
+ if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
1155
+ // This render was pinged but we didn't get to restart earlier so
1156
+ // try restarting now instead.
1157
+ root.lastPingedTime = expirationTime;
1158
+ prepareFreshStack(root, expirationTime);
1159
+ return;
1160
+ }
1161
+ }
1162
+
1163
+ const nextTime = getNextRootExpirationTimeToWorkOn(root);
1164
+ if (nextTime !== NoWork && nextTime !== expirationTime) {
1165
+ // There's additional work on this root.
1166
+ return;
1167
}
1081
- const lastPendingTime = root.lastPendingTime;
1082
- if (lastPendingTime < expirationTime) {
1083
- // There's lower priority work. It might be unsuspended. Try rendering
1084
- // at that level immediately.
1085
- return renderRoot.bind(null, root, lastPendingTime);
1168
+ if (
1169
+ lastSuspendedTime !== NoWork &&
1170
+ lastSuspendedTime !== expirationTime
1171
+ ) {
1172
+ // We should prefer to render the fallback of at the last suspended
1173
+ // level. Ping the last suspended level to try rendering it again.
1174
+ root.lastPingedTime = lastSuspendedTime;
1175
+ return;
1176
}
1177
1178
let msUntilTimeout;
1220
commitRoot.bind(null, root),
1221
msUntilTimeout,
1222
);
1133
- return null;
1223
+ return;
1224
}
1225
}
1226
// The work expired. Commit immediately.
1137
- return commitRoot.bind(null, root);
1227
+ commitRoot(root);
1228
+ return;
1229
}
1230
case RootCompleted: {
1231
// The work completed. Ready to commit.
1249
workInProgressRootCanSuspendUsingConfig,
1250
);
1251
if (msUntilTimeout > 10) {
1252
+ markRootSuspendedAtTime(root, expirationTime);
1253
root.timeoutHandle = scheduleTimeout(
1254
commitRoot.bind(null, root),
1255
msUntilTimeout,
1256
);
1165
- return null;
1257
+ return;
1258
}
1259
}
1168
- return commitRoot.bind(null, root);
1260
+ commitRoot(root);
1261
+ return;
1262
}
1263
default: {
1264
invariant(false, 'Unknown root exit status.');
1292
}
1293
}
1294
1295
+export function markUnprocessedUpdateTime(
1296
+ expirationTime: ExpirationTime,
1297
+): void {
1298
+ if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1299
+ workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1300
+ }
1301
+}
1302
+
1303
export function renderDidSuspend(): void {
1304
if (workInProgressRootExitStatus === RootIncomplete) {
1305
workInProgressRootExitStatus = RootSuspended;
1313
) {
1314
workInProgressRootExitStatus = RootSuspendedWithDelay;
1315
}
1316
+
1317
+ // Check if there's a lower priority update somewhere else in the tree.
1318
+ if (
1319
+ workInProgressRootNextUnprocessedUpdateTime !== NoWork &&
1320
+ workInProgressRoot !== null
1321
+ ) {
1322
+ // Mark the current render as suspended, and then mark that there's a
1323
+ // pending update.
1324
+ // TODO: This should immediately interrupt the current render, instead
1325
+ // of waiting until the next time we yield.
1326
+ markRootSuspendedAtTime(workInProgressRoot, renderExpirationTime);
1327
+ markRootUpdatedAtTime(
1328
+ workInProgressRoot,
1329
+ workInProgressRootNextUnprocessedUpdateTime,
1330
+ );
1331
+ }
1332
}
1333
1334
export function renderDidError() {
1543
return null;
1544
}
1545
1546
+function getRemainingExpirationTime(fiber: Fiber) {
1547
+ const updateExpirationTime = fiber.expirationTime;
1548
+ const childExpirationTime = fiber.childExpirationTime;
1549
+ return updateExpirationTime > childExpirationTime
1550
+ ? updateExpirationTime
1551
+ : childExpirationTime;
1552
+}
1553
+
1554
function resetChildExpirationTime(completedWork: Fiber) {
1555
if (
1556
renderExpirationTime !== Never &&
1653
// So we can clear these now to allow a new callback to be scheduled.
1654
root.callbackNode = null;
1655
root.callbackExpirationTime = NoWork;
1656
+ root.callbackPriority = NoPriority;
1657
+ root.nextKnownPendingLevel = NoWork;
1658
1659
startCommitTimer();
1660
1661
// Update the first and last pending times on this root. The new first
1662
// pending time is whatever is left on the root fiber.
1536
- const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
1537
- const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
1538
- const firstPendingTimeBeforeCommit =
1539
- childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
1540
- ? childExpirationTimeBeforeCommit
1541
- : updateExpirationTimeBeforeCommit;
1542
- root.firstPendingTime = firstPendingTimeBeforeCommit;
1543
- if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
1544
- // This usually means we've finished all the work, but it can also happen
1545
- // when something gets downprioritized during render, like a hidden tree.
1546
- root.lastPendingTime = firstPendingTimeBeforeCommit;
1547
- }
1663
+ const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
1664
+ finishedWork,
1665
+ );
1666
+ markRootFinishedAtTime(
1667
+ root,
1668
+ expirationTime,
1669
+ remainingExpirationTimeBeforeCommit,
1670
+ );
1671
1672
if (root === workInProgressRoot) {
1673
// We can reset these now that they are finished.
1869
// Check if there's remaining work on this root
1870
const remainingExpirationTime = root.firstPendingTime;
1871
if (remainingExpirationTime !== NoWork) {
1749
- const currentTime = requestCurrentTime();
1750
- const priorityLevel = inferPriorityFromExpirationTime(
1751
- currentTime,
1752
- remainingExpirationTime,
1753
- );
1754
-
1872
if (enableSchedulerTracing) {
1873
if (spawnedWorkDuringRender !== null) {
1874
const expirationTimes = spawnedWorkDuringRender;
1881
);
1882
}
1883
}
1884
+ schedulePendingInteractions(root, remainingExpirationTime);
1885
}
1768
-
1769
- scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
1886
} else {
1887
// If there's no remaining work, we can clear the set of already failed
1888
// error boundaries.
1899
}
1900
}
1901
1786
- onCommitRoot(finishedWork.stateNode, expirationTime);
1787
-
1902
if (remainingExpirationTime === Sync) {
1903
// Count the number of times the root synchronously re-renders without
1904
// finishing. If there are too many, it indicates an infinite update loop.
1912
nestedUpdateCount = 0;
1913
}
1914
1915
+ onCommitRoot(finishedWork.stateNode, expirationTime);
1916
+
1917
+ // Always call this before exiting `commitRoot`, to ensure that any
1918
+ // additional work on this root is scheduled.
1919
+ ensureRootIsScheduled(root);
1920
+
1921
if (hasUncaughtError) {
1922
hasUncaughtError = false;
1923
const error = firstUncaughtError;
2182
enqueueUpdate(rootFiber, update);
2183
const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2184
if (root !== null) {
2065
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2185
+ ensureRootIsScheduled(root);
2186
+ schedulePendingInteractions(root, Sync);
2187
}
2188
}
2189
2218
enqueueUpdate(fiber, update);
2219
const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2220
if (root !== null) {
2100
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2221
+ ensureRootIsScheduled(root);
2222
+ schedulePendingInteractions(root, Sync);
2223
}
2224
return;
2225
}
2271
return;
2272
}
2273
2152
- const lastPendingTime = root.lastPendingTime;
2153
- if (lastPendingTime < suspendedTime) {
2274
+ if (!isRootSuspendedAtTime(root, suspendedTime)) {
2275
// The root is no longer suspended at this time.
2276
return;
2277
}
2278
2158
- const pingTime = root.pingTime;
2159
- if (pingTime !== NoWork && pingTime < suspendedTime) {
2279
+ const lastPingedTime = root.lastPingedTime;
2280
+ if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2281
// There's already a lower priority ping scheduled.
2282
return;
2283
}
2284
2285
// Mark the time at which this ping was scheduled.
2165
- root.pingTime = suspendedTime;
2286
+ root.lastPingedTime = suspendedTime;
2287
2288
if (root.finishedExpirationTime === suspendedTime) {
2289
// If there's a pending fallback waiting to commit, throw it away.
2291
root.finishedWork = null;
2292
}
2293
2173
- const currentTime = requestCurrentTime();
2174
- const priorityLevel = inferPriorityFromExpirationTime(
2175
- currentTime,
2176
- suspendedTime,
2177
- );
2178
- scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
2294
+ ensureRootIsScheduled(root);
2295
+ schedulePendingInteractions(root, suspendedTime);
2296
}
2297
2298
function retryTimedOutBoundary(
2303
// previously was rendered in its fallback state. One of the promises that
2304
// suspended it has resolved, which means at least part of the tree was
2305
// likely unblocked. Try rendering again, at a new expiration time.
2189
- const currentTime = requestCurrentTime();
2306
if (retryTime === Never) {
2307
const suspenseConfig = null; // Retries don't carry over the already committed update.
2308
+ const currentTime = requestCurrentTime();
2309
retryTime = computeExpirationForFiber(
2310
currentTime,
2311
boundaryFiber,
2313
);
2314
}
2315
// TODO: Special case idle priority?
2199
- const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
2316
const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
2317
if (root !== null) {
2202
- scheduleCallbackForRoot(root, priorityLevel, retryTime);
2318
+ ensureRootIsScheduled(root);
2319
+ schedulePendingInteractions(root, retryTime);
2320
}
2321
}
2322