200
const RenderContext = /* */ 0b010000;
201
const CommitContext = /* */ 0b100000;
202
203
-type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
203
+type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
204
const RootIncomplete = 0;
205
-const RootErrored = 1;
206
-const RootSuspended = 2;
207
-const RootSuspendedWithDelay = 3;
208
-const RootCompleted = 4;
209
-const RootLocked = 5;
205
+const RootFatalErrored = 1;
206
+const RootErrored = 2;
207
+const RootSuspended = 3;
208
+const RootSuspendedWithDelay = 4;
209
+const RootCompleted = 5;
210
+const RootLocked = 6;
211
212
export type Thenable = {
213
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
226
let renderExpirationTime: ExpirationTime = NoWork;
227
// Whether to root completed, errored, suspended, etc.
228
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
229
+// A fatal error, if one is thrown
230
+let workInProgressRootFatalError: mixed = null;
231
// Most recent event time among processed updates during this render.
232
// This is conceptually a time stamp but expressed in terms of an ExpirationTime
233
// because we deal mostly with expiration times in the hot path, so this avoids
658
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
659
if (expirationTime !== NoWork) {
660
const originalCallbackNode = root.callbackNode;
658
- try {
659
- renderRoot(root, expirationTime, false);
661
+ invariant(
662
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
663
+ 'Should not already be working.',
664
+ );
665
+
666
+ flushPassiveEffects();
667
+
668
+ // If the root or expiration time have changed, throw out the existing stack
669
+ // and prepare a fresh one. Otherwise we'll continue where we left off.
670
+ if (
671
+ root !== workInProgressRoot ||
672
+ expirationTime !== renderExpirationTime
673
+ ) {
674
+ prepareFreshStack(root, expirationTime);
675
+ startWorkOnPendingInteractions(root, expirationTime);
676
+ }
677
+
678
+ // If we have a work-in-progress fiber, it means there's still work to do
679
+ // in this root.
680
+ if (workInProgress !== null) {
681
+ const prevExecutionContext = executionContext;
682
+ executionContext |= RenderContext;
683
+ const prevDispatcher = pushDispatcher(root);
684
+ const prevInteractions = pushInteractions(root);
685
+ startWorkLoopTimer(workInProgress);
686
+ do {
687
+ try {
688
+ workLoopConcurrent();
689
+ break;
690
+ } catch (thrownValue) {
691
+ handleError(root, thrownValue);
692
+ }
693
+ } while (true);
694
+ resetContextDependencies();
695
+ executionContext = prevExecutionContext;
696
+ popDispatcher(prevDispatcher);
697
+ if (enableSchedulerTracing) {
698
+ popInteractions(((prevInteractions: any): Set<Interaction>));
699
+ }
700
+
701
+ if (workInProgressRootExitStatus === RootFatalErrored) {
702
+ const fatalError = workInProgressRootFatalError;
703
+ stopInterruptedWorkLoopTimer();
704
+ prepareFreshStack(root, expirationTime);
705
+ markRootSuspendedAtTime(root, expirationTime);
706
+ ensureRootIsScheduled(root);
707
+ throw fatalError;
708
+ }
709
+
710
if (workInProgress !== null) {
711
// There's still work left over. Exit without committing.
712
stopInterruptedWorkLoopTimer();
718
const finishedWork: Fiber = ((root.finishedWork =
719
root.current.alternate): any);
720
root.finishedExpirationTime = expirationTime;
671
-
721
resolveLocksOnRoot(root, expirationTime);
722
+ finishConcurrentRender(
723
+ root,
724
+ finishedWork,
725
+ workInProgressRootExitStatus,
726
+ expirationTime,
727
+ );
728
+ }
729
+
730
+ ensureRootIsScheduled(root);
731
+ if (root.callbackNode === originalCallbackNode) {
732
+ // The task node scheduled for this root is the same one that's
733
+ // currently executed. Need to return a continuation.
734
+ return performConcurrentWorkOnRoot.bind(null, root);
735
+ }
736
+ }
737
+ }
738
+ return null;
739
+}
740
674
- // Set this to null to indicate there's no in-progress render.
675
- workInProgressRoot = null;
741
+function finishConcurrentRender(
742
+ root,
743
+ finishedWork,
744
+ exitStatus,
745
+ expirationTime,
746
+) {
747
+ // Set this to null to indicate there's no in-progress render.
748
+ workInProgressRoot = null;
749
677
- switch (workInProgressRootExitStatus) {
678
- case RootIncomplete: {
679
- invariant(false, 'Should have a work-in-progress.');
680
- }
681
- // Flow knows about invariant, so it complains if I add a break
682
- // statement, but eslint doesn't know about invariant, so it complains
683
- // if I do. eslint-disable-next-line no-fallthrough
684
- case RootErrored: {
685
- if (expirationTime !== Idle) {
686
- // If this was an async render, the error may have happened due to
687
- // a mutation in a concurrent event. Try rendering one more time,
688
- // synchronously, to see if the error goes away. If there are
689
- // lower priority updates, let's include those, too, in case they
690
- // fix the inconsistency. Render at Idle to include all updates.
691
- markRootExpiredAtTime(root, Idle);
750
+ switch (exitStatus) {
751
+ case RootIncomplete:
752
+ case RootFatalErrored: {
753
+ invariant(false, 'Root did not complete. This is a bug in React.');
754
+ }
755
+ // Flow knows about invariant, so it complains if I add a break
756
+ // statement, but eslint doesn't know about invariant, so it complains
757
+ // if I do. eslint-disable-next-line no-fallthrough
758
+ case RootErrored: {
759
+ if (expirationTime !== Idle) {
760
+ // If this was an async render, the error may have happened due to
761
+ // a mutation in a concurrent event. Try rendering one more time,
762
+ // synchronously, to see if the error goes away. If there are
763
+ // lower priority updates, let's include those, too, in case they
764
+ // fix the inconsistency. Render at Idle to include all updates.
765
+ markRootExpiredAtTime(root, Idle);
766
+ break;
767
+ }
768
+ // Commit the root in its errored state.
769
+ commitRoot(root);
770
+ break;
771
+ }
772
+ case RootSuspended: {
773
+ markRootSuspendedAtTime(root, expirationTime);
774
+ const lastSuspendedTime = root.lastSuspendedTime;
775
+ if (expirationTime === lastSuspendedTime) {
776
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
777
+ }
778
+ flushSuspensePriorityWarningInDEV();
779
+
780
+ // We have an acceptable loading state. We need to figure out if we
781
+ // should immediately commit it or wait a bit.
782
+
783
+ // If we have processed new updates during this render, we may now
784
+ // have a new loading state ready. We want to ensure that we commit
785
+ // that as soon as possible.
786
+ const hasNotProcessedNewUpdates =
787
+ workInProgressRootLatestProcessedExpirationTime === Sync;
788
+ if (
789
+ hasNotProcessedNewUpdates &&
790
+ // do not delay if we're inside an act() scope
791
+ !(
792
+ __DEV__ &&
793
+ flushSuspenseFallbacksInTests &&
794
+ IsThisRendererActing.current
795
+ )
796
+ ) {
797
+ // If we have not processed any new updates during this pass, then
798
+ // this is either a retry of an existing fallback state or a
799
+ // hidden tree. Hidden trees shouldn't be batched with other work
800
+ // and after that's fixed it can only be a retry. We're going to
801
+ // throttle committing retries so that we don't show too many
802
+ // loading states too quickly.
803
+ let msUntilTimeout =
804
+ globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
805
+ // Don't bother with a very short suspense time.
806
+ if (msUntilTimeout > 10) {
807
+ if (workInProgressRootHasPendingPing) {
808
+ const lastPingedTime = root.lastPingedTime;
809
+ if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
810
+ // This render was pinged but we didn't get to restart
811
+ // earlier so try restarting now instead.
812
+ root.lastPingedTime = expirationTime;
813
+ prepareFreshStack(root, expirationTime);
814
break;
815
}
694
- // Commit the root in its errored state.
695
- commitRoot(root);
816
+ }
817
+
818
+ const nextTime = getNextRootExpirationTimeToWorkOn(root);
819
+ if (nextTime !== NoWork && nextTime !== expirationTime) {
820
+ // There's additional work on this root.
821
+ break;
822
+ }
823
+ if (
824
+ lastSuspendedTime !== NoWork &&
825
+ lastSuspendedTime !== expirationTime
826
+ ) {
827
+ // We should prefer to render the fallback of at the last
828
+ // suspended level. Ping the last suspended level to try
829
+ // rendering it again.
830
+ root.lastPingedTime = lastSuspendedTime;
831
break;
832
}
698
- case RootSuspended: {
699
- markRootSuspendedAtTime(root, expirationTime);
700
- const lastSuspendedTime = root.lastSuspendedTime;
701
- if (expirationTime === lastSuspendedTime) {
702
- root.nextKnownPendingLevel = getRemainingExpirationTime(
703
- finishedWork,
704
- );
705
- }
706
- flushSuspensePriorityWarningInDEV();
707
-
708
- // We have an acceptable loading state. We need to figure out if we
709
- // should immediately commit it or wait a bit.
710
-
711
- // If we have processed new updates during this render, we may now
712
- // have a new loading state ready. We want to ensure that we commit
713
- // that as soon as possible.
714
- const hasNotProcessedNewUpdates =
715
- workInProgressRootLatestProcessedExpirationTime === Sync;
716
- if (
717
- hasNotProcessedNewUpdates &&
718
- // do not delay if we're inside an act() scope
719
- !(
720
- __DEV__ &&
721
- flushSuspenseFallbacksInTests &&
722
- IsThisRendererActing.current
723
- )
724
- ) {
725
- // If we have not processed any new updates during this pass, then
726
- // this is either a retry of an existing fallback state or a
727
- // hidden tree. Hidden trees shouldn't be batched with other work
728
- // and after that's fixed it can only be a retry. We're going to
729
- // throttle committing retries so that we don't show too many
730
- // loading states too quickly.
731
- let msUntilTimeout =
732
- globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
733
- // Don't bother with a very short suspense time.
734
- if (msUntilTimeout > 10) {
735
- if (workInProgressRootHasPendingPing) {
736
- const lastPingedTime = root.lastPingedTime;
737
- if (
738
- lastPingedTime === NoWork ||
739
- lastPingedTime >= expirationTime
740
- ) {
741
- // This render was pinged but we didn't get to restart
742
- // earlier so try restarting now instead.
743
- root.lastPingedTime = expirationTime;
744
- prepareFreshStack(root, expirationTime);
745
- break;
746
- }
747
- }
833
749
- const nextTime = getNextRootExpirationTimeToWorkOn(root);
750
- if (nextTime !== NoWork && nextTime !== expirationTime) {
751
- // There's additional work on this root.
752
- break;
753
- }
754
- if (
755
- lastSuspendedTime !== NoWork &&
756
- lastSuspendedTime !== expirationTime
757
- ) {
758
- // We should prefer to render the fallback of at the last
759
- // suspended level. Ping the last suspended level to try
760
- // rendering it again.
761
- root.lastPingedTime = lastSuspendedTime;
762
- break;
763
- }
834
+ // The render is suspended, it hasn't timed out, and there's no
835
+ // lower priority work to do. Instead of committing the fallback
836
+ // immediately, wait for more data to arrive.
837
+ root.timeoutHandle = scheduleTimeout(
838
+ commitRoot.bind(null, root),
839
+ msUntilTimeout,
840
+ );
841
+ break;
842
+ }
843
+ }
844
+ // The work expired. Commit immediately.
845
+ commitRoot(root);
846
+ break;
847
+ }
848
+ case RootSuspendedWithDelay: {
849
+ markRootSuspendedAtTime(root, expirationTime);
850
+ const lastSuspendedTime = root.lastSuspendedTime;
851
+ if (expirationTime === lastSuspendedTime) {
852
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
853
+ }
854
+ flushSuspensePriorityWarningInDEV();
855
765
- // The render is suspended, it hasn't timed out, and there's no
766
- // lower priority work to do. Instead of committing the fallback
767
- // immediately, wait for more data to arrive.
768
- root.timeoutHandle = scheduleTimeout(
769
- commitRoot.bind(null, root),
770
- msUntilTimeout,
771
- );
772
- break;
773
- }
774
- }
775
- // The work expired. Commit immediately.
776
- commitRoot(root);
856
+ if (
857
+ // do not delay if we're inside an act() scope
858
+ !(
859
+ __DEV__ &&
860
+ flushSuspenseFallbacksInTests &&
861
+ IsThisRendererActing.current
862
+ )
863
+ ) {
864
+ // We're suspended in a state that should be avoided. We'll try to
865
+ // avoid committing it for as long as the timeouts let us.
866
+ if (workInProgressRootHasPendingPing) {
867
+ const lastPingedTime = root.lastPingedTime;
868
+ if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
869
+ // This render was pinged but we didn't get to restart earlier
870
+ // so try restarting now instead.
871
+ root.lastPingedTime = expirationTime;
872
+ prepareFreshStack(root, expirationTime);
873
break;
874
}
779
- case RootSuspendedWithDelay: {
780
- markRootSuspendedAtTime(root, expirationTime);
781
- const lastSuspendedTime = root.lastSuspendedTime;
782
- if (expirationTime === lastSuspendedTime) {
783
- root.nextKnownPendingLevel = getRemainingExpirationTime(
784
- finishedWork,
785
- );
786
- }
787
- flushSuspensePriorityWarningInDEV();
788
-
789
- if (
790
- // do not delay if we're inside an act() scope
791
- !(
792
- __DEV__ &&
793
- flushSuspenseFallbacksInTests &&
794
- IsThisRendererActing.current
795
- )
796
- ) {
797
- // We're suspended in a state that should be avoided. We'll try to
798
- // avoid committing it for as long as the timeouts let us.
799
- if (workInProgressRootHasPendingPing) {
800
- const lastPingedTime = root.lastPingedTime;
801
- if (
802
- lastPingedTime === NoWork ||
803
- lastPingedTime >= expirationTime
804
- ) {
805
- // This render was pinged but we didn't get to restart earlier
806
- // so try restarting now instead.
807
- root.lastPingedTime = expirationTime;
808
- prepareFreshStack(root, expirationTime);
809
- break;
810
- }
811
- }
812
-
813
- const nextTime = getNextRootExpirationTimeToWorkOn(root);
814
- if (nextTime !== NoWork && nextTime !== expirationTime) {
815
- // There's additional work on this root.
816
- break;
817
- }
818
- if (
819
- lastSuspendedTime !== NoWork &&
820
- lastSuspendedTime !== expirationTime
821
- ) {
822
- // We should prefer to render the fallback of at the last
823
- // suspended level. Ping the last suspended level to try
824
- // rendering it again.
825
- root.lastPingedTime = lastSuspendedTime;
826
- break;
827
- }
875
+ }
876
829
- let msUntilTimeout;
830
- if (workInProgressRootLatestSuspenseTimeout !== Sync) {
831
- // We have processed a suspense config whose expiration time we
832
- // can use as the timeout.
833
- msUntilTimeout =
834
- expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) -
835
- now();
836
- } else if (
837
- workInProgressRootLatestProcessedExpirationTime === Sync
838
- ) {
839
- // This should never normally happen because only new updates
840
- // cause delayed states, so we should have processed something.
841
- // However, this could also happen in an offscreen tree.
842
- msUntilTimeout = 0;
843
- } else {
844
- // If we don't have a suspense config, we're going to use a
845
- // heuristic to determine how long we can suspend.
846
- const eventTimeMs: number = inferTimeFromExpirationTime(
847
- workInProgressRootLatestProcessedExpirationTime,
848
- );
849
- const currentTimeMs = now();
850
- const timeUntilExpirationMs =
851
- expirationTimeToMs(expirationTime) - currentTimeMs;
852
- let timeElapsed = currentTimeMs - eventTimeMs;
853
- if (timeElapsed < 0) {
854
- // We get this wrong some time since we estimate the time.
855
- timeElapsed = 0;
856
- }
877
+ const nextTime = getNextRootExpirationTimeToWorkOn(root);
878
+ if (nextTime !== NoWork && nextTime !== expirationTime) {
879
+ // There's additional work on this root.
880
+ break;
881
+ }
882
+ if (
883
+ lastSuspendedTime !== NoWork &&
884
+ lastSuspendedTime !== expirationTime
885
+ ) {
886
+ // We should prefer to render the fallback of at the last
887
+ // suspended level. Ping the last suspended level to try
888
+ // rendering it again.
889
+ root.lastPingedTime = lastSuspendedTime;
890
+ break;
891
+ }
892
858
- msUntilTimeout = jnd(timeElapsed) - timeElapsed;
893
+ let msUntilTimeout;
894
+ if (workInProgressRootLatestSuspenseTimeout !== Sync) {
895
+ // We have processed a suspense config whose expiration time we
896
+ // can use as the timeout.
897
+ msUntilTimeout =
898
+ expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
899
+ } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
900
+ // This should never normally happen because only new updates
901
+ // cause delayed states, so we should have processed something.
902
+ // However, this could also happen in an offscreen tree.
903
+ msUntilTimeout = 0;
904
+ } else {
905
+ // If we don't have a suspense config, we're going to use a
906
+ // heuristic to determine how long we can suspend.
907
+ const eventTimeMs: number = inferTimeFromExpirationTime(
908
+ workInProgressRootLatestProcessedExpirationTime,
909
+ );
910
+ const currentTimeMs = now();
911
+ const timeUntilExpirationMs =
912
+ expirationTimeToMs(expirationTime) - currentTimeMs;
913
+ let timeElapsed = currentTimeMs - eventTimeMs;
914
+ if (timeElapsed < 0) {
915
+ // We get this wrong some time since we estimate the time.
916
+ timeElapsed = 0;
917
+ }
918
860
- // Clamp the timeout to the expiration time. TODO: Once the
861
- // event time is exact instead of inferred from expiration time
862
- // we don't need this.
863
- if (timeUntilExpirationMs < msUntilTimeout) {
864
- msUntilTimeout = timeUntilExpirationMs;
865
- }
866
- }
919
+ msUntilTimeout = jnd(timeElapsed) - timeElapsed;
920
868
- // Don't bother with a very short suspense time.
869
- if (msUntilTimeout > 10) {
870
- // The render is suspended, it hasn't timed out, and there's no
871
- // lower priority work to do. Instead of committing the fallback
872
- // immediately, wait for more data to arrive.
873
- root.timeoutHandle = scheduleTimeout(
874
- commitRoot.bind(null, root),
875
- msUntilTimeout,
876
- );
877
- break;
878
- }
879
- }
880
- // The work expired. Commit immediately.
881
- commitRoot(root);
882
- break;
883
- }
884
- case RootCompleted: {
885
- // The work completed. Ready to commit.
886
- if (
887
- // do not delay if we're inside an act() scope
888
- !(
889
- __DEV__ &&
890
- flushSuspenseFallbacksInTests &&
891
- IsThisRendererActing.current
892
- ) &&
893
- workInProgressRootLatestProcessedExpirationTime !== Sync &&
894
- workInProgressRootCanSuspendUsingConfig !== null
895
- ) {
896
- // If we have exceeded the minimum loading delay, which probably
897
- // means we have shown a spinner already, we might have to suspend
898
- // a bit longer to ensure that the spinner is shown for
899
- // enough time.
900
- const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
901
- workInProgressRootLatestProcessedExpirationTime,
902
- expirationTime,
903
- workInProgressRootCanSuspendUsingConfig,
904
- );
905
- if (msUntilTimeout > 10) {
906
- markRootSuspendedAtTime(root, expirationTime);
907
- root.timeoutHandle = scheduleTimeout(
908
- commitRoot.bind(null, root),
909
- msUntilTimeout,
910
- );
911
- break;
912
- }
913
- }
914
- commitRoot(root);
915
- break;
916
- }
917
- case RootLocked: {
918
- // This root has a lock that prevents it from committing. Exit. If
919
- // we begin work on the root again, without any intervening updates,
920
- // it will finish without doing additional work.
921
- markRootSuspendedAtTime(root, expirationTime);
922
- break;
923
- }
924
- default: {
925
- invariant(false, 'Unknown root exit status.');
921
+ // Clamp the timeout to the expiration time. TODO: Once the
922
+ // event time is exact instead of inferred from expiration time
923
+ // we don't need this.
924
+ if (timeUntilExpirationMs < msUntilTimeout) {
925
+ msUntilTimeout = timeUntilExpirationMs;
926
}
927
}
928
+
929
+ // Don't bother with a very short suspense time.
930
+ if (msUntilTimeout > 10) {
931
+ // The render is suspended, it hasn't timed out, and there's no
932
+ // lower priority work to do. Instead of committing the fallback
933
+ // immediately, wait for more data to arrive.
934
+ root.timeoutHandle = scheduleTimeout(
935
+ commitRoot.bind(null, root),
936
+ msUntilTimeout,
937
+ );
938
+ break;
939
+ }
940
}
929
- // Before exiting, make sure there's a callback scheduled for the
930
- // pending level. This is intentionally duplicated in the `catch` block,
931
- // instead of using `finally`, because it needs to happen before we
932
- // possibly return a continuation, and we can't return in the `finally`
933
- // block without suppressing a potential error.
934
- ensureRootIsScheduled(root);
935
- if (root.callbackNode === originalCallbackNode) {
936
- // The task node scheduled for this root is the same one that's
937
- // currently executed. Need to return a continuation.
938
- return performConcurrentWorkOnRoot.bind(null, root);
941
+ // The work expired. Commit immediately.
942
+ commitRoot(root);
943
+ break;
944
+ }
945
+ case RootCompleted: {
946
+ // The work completed. Ready to commit.
947
+ if (
948
+ // do not delay if we're inside an act() scope
949
+ !(
950
+ __DEV__ &&
951
+ flushSuspenseFallbacksInTests &&
952
+ IsThisRendererActing.current
953
+ ) &&
954
+ workInProgressRootLatestProcessedExpirationTime !== Sync &&
955
+ workInProgressRootCanSuspendUsingConfig !== null
956
+ ) {
957
+ // If we have exceeded the minimum loading delay, which probably
958
+ // means we have shown a spinner already, we might have to suspend
959
+ // a bit longer to ensure that the spinner is shown for
960
+ // enough time.
961
+ const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
962
+ workInProgressRootLatestProcessedExpirationTime,
963
+ expirationTime,
964
+ workInProgressRootCanSuspendUsingConfig,
965
+ );
966
+ if (msUntilTimeout > 10) {
967
+ markRootSuspendedAtTime(root, expirationTime);
968
+ root.timeoutHandle = scheduleTimeout(
969
+ commitRoot.bind(null, root),
970
+ msUntilTimeout,
971
+ );
972
+ break;
973
+ }
974
}
940
- } catch (error) {
941
- ensureRootIsScheduled(root);
942
- throw error;
975
+ commitRoot(root);
976
+ break;
977
+ }
978
+ case RootLocked: {
979
+ // This root has a lock that prevents it from committing. Exit. If
980
+ // we begin work on the root again, without any intervening updates,
981
+ // it will finish without doing additional work.
982
+ markRootSuspendedAtTime(root, expirationTime);
983
+ break;
984
+ }
985
+ default: {
986
+ invariant(false, 'Unknown root exit status.');
987
}
988
}
945
- return null;
989
}
990
991
// This is the entry point for synchronous tasks that don't go
994
// Check if there's expired work on this root. Otherwise, render at Sync.
995
const lastExpiredTime = root.lastExpiredTime;
996
const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
954
- try {
955
- if (root.finishedExpirationTime === expirationTime) {
956
- // There's already a pending commit at this expiration time.
957
- // TODO: This is poorly factored. This case only exists for the
958
- // batch.commit() API.
959
- commitRoot(root);
960
- } else {
961
- renderRoot(root, expirationTime, true);
962
- invariant(
963
- workInProgressRootExitStatus !== RootIncomplete,
964
- 'Cannot commit an incomplete root. This error is likely caused by a ' +
965
- 'bug in React. Please file an issue.',
966
- );
997
+ if (root.finishedExpirationTime === expirationTime) {
998
+ // There's already a pending commit at this expiration time.
999
+ // TODO: This is poorly factored. This case only exists for the
1000
+ // batch.commit() API.
1001
+ commitRoot(root);
1002
+ } else {
1003
+ invariant(
1004
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
1005
+ 'Should not already be working.',
1006
+ );
1007
968
- // We now have a consistent tree. The next step is either to commit it,
969
- // or, if something suspended, wait to commit it after a timeout.
970
- stopFinishedWorkLoopTimer();
1008
+ flushPassiveEffects();
1009
972
- root.finishedWork = ((root.current.alternate: any): Fiber);
973
- root.finishedExpirationTime = expirationTime;
1010
+ // If the root or expiration time have changed, throw out the existing stack
1011
+ // and prepare a fresh one. Otherwise we'll continue where we left off.
1012
+ if (
1013
+ root !== workInProgressRoot ||
1014
+ expirationTime !== renderExpirationTime
1015
+ ) {
1016
+ prepareFreshStack(root, expirationTime);
1017
+ startWorkOnPendingInteractions(root, expirationTime);
1018
+ }
1019
975
- resolveLocksOnRoot(root, expirationTime);
976
- if (workInProgressRootExitStatus === RootLocked) {
977
- // This root has a lock that prevents it from committing. Exit. If we
978
- // begin work on the root again, without any intervening updates, it
979
- // will finish without doing additional work.
980
- markRootSuspendedAtTime(root, expirationTime);
981
- } else {
982
- // Set this to null to indicate there's no in-progress render.
983
- workInProgressRoot = null;
1020
+ // If we have a work-in-progress fiber, it means there's still work to do
1021
+ // in this root.
1022
+ if (workInProgress !== null) {
1023
+ const prevExecutionContext = executionContext;
1024
+ executionContext |= RenderContext;
1025
+ const prevDispatcher = pushDispatcher(root);
1026
+ const prevInteractions = pushInteractions(root);
1027
+ startWorkLoopTimer(workInProgress);
1028
985
- if (__DEV__) {
986
- if (
987
- workInProgressRootExitStatus === RootSuspended ||
988
- workInProgressRootExitStatus === RootSuspendedWithDelay
989
- ) {
990
- flushSuspensePriorityWarningInDEV();
991
- }
1029
+ do {
1030
+ try {
1031
+ workLoopSync();
1032
+ break;
1033
+ } catch (thrownValue) {
1034
+ handleError(root, thrownValue);
1035
}
993
- commitRoot(root);
1036
+ } while (true);
1037
+ resetContextDependencies();
1038
+ executionContext = prevExecutionContext;
1039
+ popDispatcher(prevDispatcher);
1040
+ if (enableSchedulerTracing) {
1041
+ popInteractions(((prevInteractions: any): Set<Interaction>));
1042
+ }
1043
+
1044
+ if (workInProgressRootExitStatus === RootFatalErrored) {
1045
+ const fatalError = workInProgressRootFatalError;
1046
+ stopInterruptedWorkLoopTimer();
1047
+ prepareFreshStack(root, expirationTime);
1048
+ markRootSuspendedAtTime(root, expirationTime);
1049
+ ensureRootIsScheduled(root);
1050
+ throw fatalError;
1051
}
1052
+
1053
+ if (workInProgress !== null) {
1054
+ // This is a sync render, so we should have finished the whole tree.
1055
+ invariant(
1056
+ false,
1057
+ 'Cannot commit an incomplete root. This error is likely caused by a ' +
1058
+ 'bug in React. Please file an issue.',
1059
+ );
1060
+ } else {
1061
+ // We now have a consistent tree. Because this is a sync render, we
1062
+ // will commit it even if something suspended. The only exception is
1063
+ // if the root is locked (using the unstable_createBatch API).
1064
+ stopFinishedWorkLoopTimer();
1065
+ root.finishedWork = (root.current.alternate: any);
1066
+ root.finishedExpirationTime = expirationTime;
1067
+ resolveLocksOnRoot(root, expirationTime);
1068
+ finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
1069
+ }
1070
+
1071
+ // Before exiting, make sure there's a callback scheduled for the next
1072
+ // pending level.
1073
+ ensureRootIsScheduled(root);
1074
}
996
- } finally {
997
- // Before exiting, make sure there's a callback scheduled for the
998
- // pending level.
999
- ensureRootIsScheduled(root);
1075
}
1076
+
1077
return null;
1078
}
1079
1080
+function finishSyncRender(root, exitStatus, expirationTime) {
1081
+ if (exitStatus === RootLocked) {
1082
+ // This root has a lock that prevents it from committing. Exit. If we
1083
+ // begin work on the root again, without any intervening updates, it
1084
+ // will finish without doing additional work.
1085
+ markRootSuspendedAtTime(root, expirationTime);
1086
+ } else {
1087
+ // Set this to null to indicate there's no in-progress render.
1088
+ workInProgressRoot = null;
1089
+
1090
+ if (__DEV__) {
1091
+ if (
1092
+ exitStatus === RootSuspended ||
1093
+ exitStatus === RootSuspendedWithDelay
1094
+ ) {
1095
+ flushSuspensePriorityWarningInDEV();
1096
+ }
1097
+ }
1098
+ commitRoot(root);
1099
+ }
1100
+}
1101
+
1102
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1103
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
1104
invariant(
1305
workInProgress = createWorkInProgress(root.current, null, expirationTime);
1306
renderExpirationTime = expirationTime;
1307
workInProgressRootExitStatus = RootIncomplete;
1308
+ workInProgressRootFatalError = null;
1309
workInProgressRootLatestProcessedExpirationTime = Sync;
1310
workInProgressRootLatestSuspenseTimeout = Sync;
1311
workInProgressRootCanSuspendUsingConfig = null;
1322
}
1323
}
1324
1226
-// renderRoot should only be called from inside either
1227
-// `performConcurrentWorkOnRoot` or `performSyncWorkOnRoot`.
1228
-function renderRoot(
1229
- root: FiberRoot,
1230
- expirationTime: ExpirationTime,
1231
- isSync: boolean,
1232
-): void {
1233
- invariant(
1234
- (executionContext & (RenderContext | CommitContext)) === NoContext,
1235
- 'Should not already be working.',
1236
- );
1325
+function handleError(root, thrownValue) {
1326
+ do {
1327
+ try {
1328
+ // Reset module-level state that was set during the render phase.
1329
+ resetContextDependencies();
1330
+ resetHooks();
1331
1238
- flushPassiveEffects();
1332
+ if (workInProgress === null || workInProgress.return === null) {
1333
+ // Expected to be working on a non-root fiber. This is a fatal error
1334
+ // because there's no ancestor that can handle it; the root is
1335
+ // supposed to capture all errors that weren't caught by an error
1336
+ // boundary.
1337
+ workInProgressRootExitStatus = RootFatalErrored;
1338
+ workInProgressRootFatalError = thrownValue;
1339
+ return null;
1340
+ }
1341
1240
- // If the root or expiration time have changed, throw out the existing stack
1241
- // and prepare a fresh one. Otherwise we'll continue where we left off.
1242
- if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1243
- prepareFreshStack(root, expirationTime);
1244
- startWorkOnPendingInteractions(root, expirationTime);
1245
- }
1342
+ if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1343
+ // Record the time spent rendering before an error was thrown. This
1344
+ // avoids inaccurate Profiler durations in the case of a
1345
+ // suspended render.
1346
+ stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
1347
+ }
1348
1247
- // If we have a work-in-progress fiber, it means there's still work to do
1248
- // in this root.
1249
- if (workInProgress !== null) {
1250
- const prevExecutionContext = executionContext;
1251
- executionContext |= RenderContext;
1252
- let prevDispatcher = ReactCurrentDispatcher.current;
1253
- if (prevDispatcher === null) {
1254
- // The React isomorphic package does not include a default dispatcher.
1255
- // Instead the first renderer will lazily attach one, in order to give
1256
- // nicer error messages.
1257
- prevDispatcher = ContextOnlyDispatcher;
1258
- }
1259
- ReactCurrentDispatcher.current = ContextOnlyDispatcher;
1260
- let prevInteractions: Set<Interaction> | null = null;
1261
- if (enableSchedulerTracing) {
1262
- prevInteractions = __interactionsRef.current;
1263
- __interactionsRef.current = root.memoizedInteractions;
1349
+ throwException(
1350
+ root,
1351
+ workInProgress.return,
1352
+ workInProgress,
1353
+ thrownValue,
1354
+ renderExpirationTime,
1355
+ );
1356
+ workInProgress = completeUnitOfWork(workInProgress);
1357
+ } catch (yetAnotherThrownValue) {
1358
+ // Something in the return path also threw.
1359
+ thrownValue = yetAnotherThrownValue;
1360
+ continue;
1361
}
1362
+ // Return to the normal work loop.
1363
+ return;
1364
+ } while (true);
1365
+}
1366
1266
- startWorkLoopTimer(workInProgress);
1267
-
1268
- do {
1269
- try {
1270
- // TODO: This is now the only place that `isSync` is used. Consider
1271
- // outlining the contents of `renderRoot`.
1272
- if (isSync) {
1273
- workLoopSync();
1274
- } else {
1275
- workLoop();
1276
- }
1277
- break;
1278
- } catch (thrownValue) {
1279
- // Reset module-level state that was set during the render phase.
1280
- resetContextDependencies();
1281
- resetHooks();
1282
-
1283
- const sourceFiber = workInProgress;
1284
- if (sourceFiber === null || sourceFiber.return === null) {
1285
- // Expected to be working on a non-root fiber. This is a fatal error
1286
- // because there's no ancestor that can handle it; the root is
1287
- // supposed to capture all errors that weren't caught by an error
1288
- // boundary.
1289
- prepareFreshStack(root, expirationTime);
1290
- executionContext = prevExecutionContext;
1291
- markRootSuspendedAtTime(root, expirationTime);
1292
- throw thrownValue;
1293
- }
1367
+function pushDispatcher(root) {
1368
+ const prevDispatcher = ReactCurrentDispatcher.current;
1369
+ ReactCurrentDispatcher.current = ContextOnlyDispatcher;
1370
+ if (prevDispatcher === null) {
1371
+ // The React isomorphic package does not include a default dispatcher.
1372
+ // Instead the first renderer will lazily attach one, in order to give
1373
+ // nicer error messages.
1374
+ return ContextOnlyDispatcher;
1375
+ } else {
1376
+ return prevDispatcher;
1377
+ }
1378
+}
1379
1295
- if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {
1296
- // Record the time spent rendering before an error was thrown. This
1297
- // avoids inaccurate Profiler durations in the case of a
1298
- // suspended render.
1299
- stopProfilerTimerIfRunningAndRecordDelta(sourceFiber, true);
1300
- }
1380
+function popDispatcher(prevDispatcher) {
1381
+ ReactCurrentDispatcher.current = prevDispatcher;
1382
+}
1383
1302
- const returnFiber = sourceFiber.return;
1303
- throwException(
1304
- root,
1305
- returnFiber,
1306
- sourceFiber,
1307
- thrownValue,
1308
- renderExpirationTime,
1309
- );
1310
- // TODO: This is not wrapped in a try-catch, so if the complete phase
1311
- // throws, we won't capture it.
1312
- workInProgress = completeUnitOfWork(sourceFiber);
1313
- }
1314
- } while (true);
1384
+function pushInteractions(root) {
1385
+ if (enableSchedulerTracing) {
1386
+ const prevInteractions: Set<Interaction> | null = __interactionsRef.current;
1387
+ __interactionsRef.current = root.memoizedInteractions;
1388
+ return prevInteractions;
1389
+ }
1390
+ return null;
1391
+}
1392
1316
- executionContext = prevExecutionContext;
1317
- resetContextDependencies();
1318
- ReactCurrentDispatcher.current = prevDispatcher;
1319
- if (enableSchedulerTracing) {
1320
- __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1321
- }
1393
+function popInteractions(prevInteractions) {
1394
+ if (enableSchedulerTracing) {
1395
+ __interactionsRef.current = prevInteractions;
1396
}
1397
}
1398
1506
}
1507
1508
/** @noinline */
1435
-function workLoop() {
1509
+function workLoopConcurrent() {
1510
// Perform work until Scheduler asks us to yield
1511
while (workInProgress !== null && !shouldYield()) {
1512
workInProgress = performUnitOfWork(workInProgress);
1834
if (firstEffect !== null) {
1835
const prevExecutionContext = executionContext;
1836
executionContext |= CommitContext;
1763
- let prevInteractions: Set<Interaction> | null = null;
1764
- if (enableSchedulerTracing) {
1765
- prevInteractions = __interactionsRef.current;
1766
- __interactionsRef.current = root.memoizedInteractions;
1767
- }
1837
+ const prevInteractions = pushInteractions(root);
1838
1839
// Reset this to null before calling lifecycles
1840
ReactCurrentOwner.current = null;
1952
requestPaint();
1953
1954
if (enableSchedulerTracing) {
1885
- __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1955
+ popInteractions(((prevInteractions: any): Set<Interaction>));
1956
}
1957
executionContext = prevExecutionContext;
1958
} else {
2221
}
2222
2223
function flushPassiveEffectsImpl(root, expirationTime) {
2154
- let prevInteractions: Set<Interaction> | null = null;
2155
- if (enableSchedulerTracing) {
2156
- prevInteractions = __interactionsRef.current;
2157
- __interactionsRef.current = root.memoizedInteractions;
2158
- }
2159
-
2224
invariant(
2225
(executionContext & (RenderContext | CommitContext)) === NoContext,
2226
'Cannot flush passive effects while already rendering.',
2227
);
2228
const prevExecutionContext = executionContext;
2229
executionContext |= CommitContext;
2230
+ const prevInteractions = pushInteractions(root);
2231
2232
// Note: This currently assumes there are no passive effects on the root
2233
// fiber, because the root is not part of its own effect list. This could
2258
}
2259
2260
if (enableSchedulerTracing) {
2196
- __interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
2261
+ popInteractions(((prevInteractions: any): Set<Interaction>));
2262
finishPendingInteractions(root, expirationTime);
2263
}
2264