Allow performWork to be called recursively
Currently we assume that performWork is never called recursively. Ideally that is the case. We shouldn't rely on recursion anywhere in Fiber. However, to support the use of syncUpdates as an opt-in to forced- synchronous updates, we need the ability to call performWork recursively. At the beginning of performWork, the state of the scheduler is saved; before exiting, that state is restored, so that the scheduler can continue where it left off. I've decided to use local variables to stash the previous state. We could also store them in a record and push it to a stack, but this approach has the advantage of being isolated to performWork.
Andrew Clark committed
Dec 28, 2016 at 21:02 UTC
8090cf59e38653bf611d04f510b3ca32434e3d47
1 file changed
+38
-19
src/renderers/shared/fiber/ReactFiberScheduler.js
+38
-19
@@ -672,10 +672,29 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
672
}
673
674
function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
675
- if (isPerformingWork) {
676
- throw new Error('performWork was called recursively.');
677
- }
675
+ // If performWork is called recursively, we need to save the previous state
676
+ // of the scheduler so it can be restored before the function exits.
677
+ // Recursion is only possible when using syncUpdates.
678
+ const previousPriorityContext = priorityContext;
679
+ const previousPriorityContextBeforeReconciliation = priorityContextBeforeReconciliation;
680
+ const previousIsPerformingWork = isPerformingWork;
681
+ const previousNextEffect = nextEffect;
682
+ const previousCommitPhaseBoundaries = commitPhaseBoundaries;
683
+ const previousFirstUncaughtError = firstUncaughtError;
684
+ const previousFatalError = fatalError;
685
+ const previousIsCommitting = isCommitting;
686
+ const previousIsUnmounting = isUnmounting;
687
+
688
+ priorityContext = NoWork;
689
+ priorityContextBeforeReconciliation = NoWork;
690
isPerformingWork = true;
691
+ nextEffect = null;
692
+ commitPhaseBoundaries = null;
693
+ firstUncaughtError = null;
694
+ fatalError = null;
695
+ isCommitting = false;
696
+ isUnmounting = false;
697
+
698
const isPerformingDeferredWork = Boolean(deadline);
699
let deadlineHasExpired = false;
700
@@ -779,22 +798,22 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
798
}
799
}
800
782
- // We're done performing work. Time to clean up.
783
- isPerformingWork = false;
784
- capturedErrors = null;
785
- failedBoundaries = null;
786
-
787
- // It's now safe to throw errors.
788
- if (fatalError) {
789
- let e = fatalError;
790
- fatalError = null;
791
- firstUncaughtError = null;
792
- throw e;
793
- }
794
- if (firstUncaughtError) {
795
- let e = firstUncaughtError;
796
- firstUncaughtError = null;
797
- throw e;
801
+ const errorToThrow = fatalError || firstUncaughtError;
802
+
803
+ // We're done performing work. Restore the previous state of the scheduler.
804
+ priorityContext = previousPriorityContext;
805
+ priorityContextBeforeReconciliation = previousPriorityContextBeforeReconciliation;
806
+ isPerformingWork = previousIsPerformingWork;
807
+ nextEffect = previousNextEffect;
808
+ commitPhaseBoundaries = previousCommitPhaseBoundaries;
809
+ firstUncaughtError = previousFirstUncaughtError;
810
+ fatalError = previousFatalError;
811
+ isCommitting = previousIsCommitting;
812
+ isUnmounting = previousIsUnmounting;
813
+
814
+ // It's safe to throw any unhandled errors.
815
+ if (errorToThrow) {
816
+ throw errorToThrow;
817
}
818
}
819