10
import type {Fiber} from './ReactFiber';
11
import type {FiberRoot} from './ReactFiberRoot';
12
import type {ExpirationTime} from './ReactFiberExpirationTime';
13
-import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
13
+import type {
14
+ ReactPriorityLevel,
15
+ SchedulerCallback,
16
+} from './SchedulerWithReactIntegration';
17
import type {Interaction} from 'scheduler/src/Tracing';
18
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
19
import type {SuspenseState} from './ReactFiberSuspenseComponent';
66
} from './ReactFiberHostConfig';
67
68
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
66
-import {
67
- isRootSuspendedAtTime,
68
- markRootSuspendedAtTime,
69
- markRootFinishedAtTime,
70
- markRootUpdatedAtTime,
71
- markRootExpiredAtTime,
72
-} from './ReactFiberRoot';
69
import {
70
NoMode,
71
StrictMode,
112
inferPriorityFromExpirationTime,
113
LOW_PRIORITY_EXPIRATION,
114
Batched,
119
- Idle,
115
} from './ReactFiberExpirationTime';
116
import {beginWork as originalBeginWork} from './ReactFiberBeginWork';
117
import {completeWork} from './ReactFiberCompleteWork';
223
let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
224
let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
225
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
-
226
// If we're pinged while rendering we don't always restart immediately.
227
// This flag determines if it might be worthwhile to restart if an opportunity
228
// happens latere.
378
return;
379
}
380
381
+ root.pingTime = NoWork;
382
+
383
checkForInterruption(fiber, expirationTime);
384
recordScheduleUpdate();
385
400
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
401
// root inside of batchedUpdates should be synchronous, but layout updates
402
// should be deferred until the end of the batch.
410
- performSyncWorkOnRoot(root);
403
+ let callback = renderRoot(root, Sync, true);
404
+ while (callback !== null) {
405
+ callback = callback(true);
406
+ }
407
} else {
412
- ensureRootIsScheduled(root);
413
- schedulePendingInteractions(root, expirationTime);
408
+ scheduleCallbackForRoot(root, ImmediatePriority, Sync);
409
if (executionContext === NoContext) {
410
// Flush the synchronous work now, unless we're already working or inside
411
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
416
}
417
}
418
} else {
424
- ensureRootIsScheduled(root);
425
- schedulePendingInteractions(root, expirationTime);
419
+ scheduleCallbackForRoot(root, priorityLevel, expirationTime);
420
}
421
422
if (
484
}
485
486
if (root !== null) {
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
- }
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;
495
}
515
- // Mark that the root has a pending update.
516
- markRootUpdatedAtTime(root, expirationTime);
496
}
497
498
return root;
499
}
500
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.
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;
518
if (existingCallbackNode !== null) {
572
- root.callbackNode = null;
573
- root.callbackExpirationTime = NoWork;
574
- root.callbackPriority = NoPriority;
519
+ cancelCallback(existingCallbackNode);
520
}
576
- return;
577
- }
521
+ root.callbackExpirationTime = expirationTime;
522
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
- );
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
+ }
541
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;
542
+ root.callbackNode = scheduleCallback(
543
+ priorityLevel,
544
+ runRootCallback.bind(
545
+ null,
546
+ root,
547
+ renderRoot.bind(null, root, expirationTime),
548
+ ),
549
+ options,
550
+ );
551
}
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
- );
552
}
553
629
- root.callbackNode = callbackNode;
630
-}
631
-
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;
554
+ // Associate the current interactions with this new root+priority.
555
+ schedulePendingInteractions(root, expirationTime);
556
}
557
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;
558
+function runRootCallback(root, callback, isSync) {
559
+ const prevCallbackNode = root.callbackNode;
560
+ let continuation = null;
561
try {
673
- renderRoot(root, expirationTime, true);
562
+ continuation = callback(isSync);
563
+ if (continuation !== null) {
564
+ return runRootCallback.bind(null, root, continuation);
565
+ } else {
566
+ return null;
567
+ }
568
} finally {
675
- // Before exiting, make sure there's a callback scheduled for the
676
- // pending level.
677
- ensureRootIsScheduled(root);
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
+ }
577
}
679
- return null;
578
}
579
580
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
585
'means you attempted to commit from inside a lifecycle method.',
586
);
587
}
690
- markRootExpiredAtTime(root, expirationTime);
691
- ensureRootIsScheduled(root);
588
+ scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
589
flushSyncCallbackQueue();
590
}
591
654
const roots = rootsWithPendingDiscreteUpdates;
655
rootsWithPendingDiscreteUpdates = null;
656
roots.forEach((expirationTime, root) => {
760
- markRootExpiredAtTime(root, expirationTime);
761
- ensureRootIsScheduled(root);
657
+ scheduleSyncCallback(renderRoot.bind(null, root, expirationTime));
658
});
659
// Now flush the immediate queue.
660
flushSyncCallbackQueue();
786
workInProgressRootLatestProcessedExpirationTime = Sync;
787
workInProgressRootLatestSuspenseTimeout = Sync;
788
workInProgressRootCanSuspendUsingConfig = null;
893
- workInProgressRootNextUnprocessedUpdateTime = NoWork;
789
workInProgressRootHasPendingPing = false;
790
791
if (enableSchedulerTracing) {
798
}
799
}
800
906
-// renderRoot should only be called from inside either
907
-// `performConcurrentWorkOnRoot` or `performSyncWorkOnRoot`.
801
function renderRoot(
802
root: FiberRoot,
803
expirationTime: ExpirationTime,
804
isSync: boolean,
912
-): void {
805
+): SchedulerCallback | null {
806
invariant(
807
(executionContext & (RenderContext | CommitContext)) === NoContext,
808
'Should not already be working.',
809
);
810
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
+
818
if (isSync && root.finishedExpirationTime === expirationTime) {
819
// There's already a pending commit at this expiration time.
820
// TODO: This is poorly factored. This case only exists for the
821
// batch.commit() API.
922
- commitRoot(root);
923
- return;
822
+ return commitRoot.bind(null, root);
823
}
824
825
flushPassiveEffects();
829
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
830
prepareFreshStack(root, expirationTime);
831
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
+ }
852
}
853
854
// If we have a work-in-progress fiber, it means there's still work to do
872
873
startWorkLoopTimer(workInProgress);
874
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
+
901
do {
902
try {
903
if (isSync) {
919
// boundary.
920
prepareFreshStack(root, expirationTime);
921
executionContext = prevExecutionContext;
977
- markRootSuspendedAtTime(root, expirationTime);
922
throw thrownValue;
923
}
924
937
thrownValue,
938
renderExpirationTime,
939
);
996
- // TODO: This is not wrapped in a try-catch, so if the complete phase
997
- // throws, we won't capture it.
940
workInProgress = completeUnitOfWork(sourceFiber);
941
}
942
} while (true);
949
}
950
951
if (workInProgress !== null) {
1010
- // There's still work left over. Exit without committing.
952
+ // There's still work left over. Return a continuation.
953
stopInterruptedWorkLoopTimer();
1012
- return;
954
+ return renderRoot.bind(null, root, expirationTime);
955
}
956
}
957
959
// something suspended, wait to commit it after a timeout.
960
stopFinishedWorkLoopTimer();
961
1020
- const finishedWork: Fiber = ((root.finishedWork =
1021
- root.current.alternate): any);
962
+ root.finishedWork = root.current.alternate;
963
root.finishedExpirationTime = expirationTime;
964
965
const isLocked = resolveLocksOnRoot(root, expirationTime);
967
// This root has a lock that prevents it from committing. Exit. If we begin
968
// work on the root again, without any intervening updates, it will finish
969
// without doing additional work.
1029
- markRootSuspendedAtTime(root, expirationTime);
1030
- return;
970
+ return null;
971
}
972
973
// Set this to null to indicate there's no in-progress render.
981
// but eslint doesn't know about invariant, so it complains if I do.
982
// eslint-disable-next-line no-fallthrough
983
case RootErrored: {
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;
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
}
1053
- // Commit the root in its errored state.
1054
- commitRoot(root);
1055
- return;
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;
1000
+ }
1001
+ // If we're already rendering synchronously, commit the root in its
1002
+ // errored state.
1003
+ return commitRoot.bind(null, root);
1004
}
1005
case RootSuspended: {
1058
- markRootSuspendedAtTime(root, expirationTime);
1059
- const lastSuspendedTime = root.lastSuspendedTime;
1060
- if (expirationTime === lastSuspendedTime) {
1061
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1062
- }
1006
flushSuspensePriorityWarningInDEV();
1007
1008
// We have an acceptable loading state. We need to figure out if we should
1034
// Don't bother with a very short suspense time.
1035
if (msUntilTimeout > 10) {
1036
if (workInProgressRootHasPendingPing) {
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
- }
1103
-
1104
- const nextTime = getNextRootExpirationTimeToWorkOn(root);
1105
- if (nextTime !== NoWork && nextTime !== expirationTime) {
1106
- // There's additional work on this root.
1107
- return;
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);
1041
}
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;
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);
1047
}
1118
-
1048
// The render is suspended, it hasn't timed out, and there's no lower
1049
// priority work to do. Instead of committing the fallback
1050
// immediately, wait for more data to arrive.
1052
commitRoot.bind(null, root),
1053
msUntilTimeout,
1054
);
1126
- return;
1055
+ return null;
1056
}
1057
}
1058
// The work expired. Commit immediately.
1130
- commitRoot(root);
1131
- return;
1059
+ return commitRoot.bind(null, root);
1060
}
1061
case RootSuspendedWithDelay: {
1134
- markRootSuspendedAtTime(root, expirationTime);
1135
- const lastSuspendedTime = root.lastSuspendedTime;
1136
- if (expirationTime === lastSuspendedTime) {
1137
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1138
- }
1062
flushSuspensePriorityWarningInDEV();
1063
1064
if (
1073
// We're suspended in a state that should be avoided. We'll try to avoid committing
1074
// it for as long as the timeouts let us.
1075
if (workInProgressRootHasPendingPing) {
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;
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);
1080
}
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;
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);
1086
}
1087
1088
let msUntilTimeout;
1130
commitRoot.bind(null, root),
1131
msUntilTimeout,
1132
);
1223
- return;
1133
+ return null;
1134
}
1135
}
1136
// The work expired. Commit immediately.
1227
- commitRoot(root);
1228
- return;
1137
+ return commitRoot.bind(null, root);
1138
}
1139
case RootCompleted: {
1140
// The work completed. Ready to commit.
1158
workInProgressRootCanSuspendUsingConfig,
1159
);
1160
if (msUntilTimeout > 10) {
1252
- markRootSuspendedAtTime(root, expirationTime);
1161
root.timeoutHandle = scheduleTimeout(
1162
commitRoot.bind(null, root),
1163
msUntilTimeout,
1164
);
1257
- return;
1165
+ return null;
1166
}
1167
}
1260
- commitRoot(root);
1261
- return;
1168
+ return commitRoot.bind(null, root);
1169
}
1170
default: {
1171
invariant(false, 'Unknown root exit status.');
1199
}
1200
}
1201
1295
-export function markUnprocessedUpdateTime(
1296
- expirationTime: ExpirationTime,
1297
-): void {
1298
- if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1299
- workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1300
- }
1301
-}
1302
-
1202
export function renderDidSuspend(): void {
1203
if (workInProgressRootExitStatus === RootIncomplete) {
1204
workInProgressRootExitStatus = RootSuspended;
1212
) {
1213
workInProgressRootExitStatus = RootSuspendedWithDelay;
1214
}
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
- }
1215
}
1216
1217
export function renderDidError() {
1426
return null;
1427
}
1428
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
-
1429
function resetChildExpirationTime(completedWork: Fiber) {
1430
if (
1431
renderExpirationTime !== Never &&
1528
// So we can clear these now to allow a new callback to be scheduled.
1529
root.callbackNode = null;
1530
root.callbackExpirationTime = NoWork;
1656
- root.callbackPriority = NoPriority;
1657
- root.nextKnownPendingLevel = NoWork;
1531
1532
startCommitTimer();
1533
1534
// Update the first and last pending times on this root. The new first
1535
// pending time is whatever is left on the root fiber.
1663
- const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
1664
- finishedWork,
1665
- );
1666
- markRootFinishedAtTime(
1667
- root,
1668
- expirationTime,
1669
- remainingExpirationTimeBeforeCommit,
1670
- );
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
+ }
1548
1549
if (root === workInProgressRoot) {
1550
// We can reset these now that they are finished.
1746
// Check if there's remaining work on this root
1747
const remainingExpirationTime = root.firstPendingTime;
1748
if (remainingExpirationTime !== NoWork) {
1749
+ const currentTime = requestCurrentTime();
1750
+ const priorityLevel = inferPriorityFromExpirationTime(
1751
+ currentTime,
1752
+ remainingExpirationTime,
1753
+ );
1754
+
1755
if (enableSchedulerTracing) {
1756
if (spawnedWorkDuringRender !== null) {
1757
const expirationTimes = spawnedWorkDuringRender;
1764
);
1765
}
1766
}
1884
- schedulePendingInteractions(root, remainingExpirationTime);
1767
}
1768
+
1769
+ scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
1770
} else {
1771
// If there's no remaining work, we can clear the set of already failed
1772
// error boundaries.
1783
}
1784
}
1785
1786
+ onCommitRoot(finishedWork.stateNode, expirationTime);
1787
+
1788
if (remainingExpirationTime === Sync) {
1789
// Count the number of times the root synchronously re-renders without
1790
// finishing. If there are too many, it indicates an infinite update loop.
1798
nestedUpdateCount = 0;
1799
}
1800
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
-
1801
if (hasUncaughtError) {
1802
hasUncaughtError = false;
1803
const error = firstUncaughtError;
2062
enqueueUpdate(rootFiber, update);
2063
const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2064
if (root !== null) {
2185
- ensureRootIsScheduled(root);
2186
- schedulePendingInteractions(root, Sync);
2065
+ scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2066
}
2067
}
2068
2097
enqueueUpdate(fiber, update);
2098
const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2099
if (root !== null) {
2221
- ensureRootIsScheduled(root);
2222
- schedulePendingInteractions(root, Sync);
2100
+ scheduleCallbackForRoot(root, ImmediatePriority, Sync);
2101
}
2102
return;
2103
}
2149
return;
2150
}
2151
2274
- if (!isRootSuspendedAtTime(root, suspendedTime)) {
2152
+ const lastPendingTime = root.lastPendingTime;
2153
+ if (lastPendingTime < suspendedTime) {
2154
// The root is no longer suspended at this time.
2155
return;
2156
}
2157
2279
- const lastPingedTime = root.lastPingedTime;
2280
- if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2158
+ const pingTime = root.pingTime;
2159
+ if (pingTime !== NoWork && pingTime < suspendedTime) {
2160
// There's already a lower priority ping scheduled.
2161
return;
2162
}
2163
2164
// Mark the time at which this ping was scheduled.
2286
- root.lastPingedTime = suspendedTime;
2165
+ root.pingTime = suspendedTime;
2166
2167
if (root.finishedExpirationTime === suspendedTime) {
2168
// If there's a pending fallback waiting to commit, throw it away.
2170
root.finishedWork = null;
2171
}
2172
2294
- ensureRootIsScheduled(root);
2295
- schedulePendingInteractions(root, suspendedTime);
2173
+ const currentTime = requestCurrentTime();
2174
+ const priorityLevel = inferPriorityFromExpirationTime(
2175
+ currentTime,
2176
+ suspendedTime,
2177
+ );
2178
+ scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
2179
}
2180
2181
function retryTimedOutBoundary(
2186
// previously was rendered in its fallback state. One of the promises that
2187
// suspended it has resolved, which means at least part of the tree was
2188
// likely unblocked. Try rendering again, at a new expiration time.
2189
+ const currentTime = requestCurrentTime();
2190
if (retryTime === Never) {
2191
const suspenseConfig = null; // Retries don't carry over the already committed update.
2308
- const currentTime = requestCurrentTime();
2192
retryTime = computeExpirationForFiber(
2193
currentTime,
2194
boundaryFiber,
2196
);
2197
}
2198
// TODO: Special case idle priority?
2199
+ const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
2200
const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
2201
if (root !== null) {
2318
- ensureRootIsScheduled(root);
2319
- schedulePendingInteractions(root, retryTime);
2202
+ scheduleCallbackForRoot(root, priorityLevel, retryTime);
2203
}
2204
}
2205