60
Never,
61
msToExpirationTime,
62
computeExpirationBucket,
63
- relativeExpirationTime,
63
} = require('ReactFiberExpirationTime');
64
65
var {AsyncUpdates} = require('ReactTypeOfInternalContext');
150
};
151
}
152
154
-var timeHeuristicForUnitOfWork = 1;
155
-
153
module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
154
config: HostConfig<T, P, I, TI, PI, C, CC, CX, PL>,
155
) {
196
// updates in sync mode.)
197
let expirationContext: ExpirationTime = NoWork;
198
202
- // Keeps track of whether we're currently in a work loop.
203
- let isPerformingWork: boolean = false;
204
-
205
- // Keeps track of whether the current deadline has expired.
206
- let deadlineHasExpired: boolean = false;
207
-
208
- // Keeps track of whether we should should batch sync updates.
209
- let isBatchingUpdates: boolean = false;
210
-
211
- // This is needed for the weird case where the initial mount is synchronous
212
- // even inside batchedUpdates :(
213
- let isUnbatchingUpdates: boolean = false;
199
+ let isWorking: boolean = false;
200
201
// The next work in progress fiber that we're currently working on.
202
let nextUnitOfWork: Fiber | null = null;
203
+ let nextRoot: FiberRoot | null = null;
204
// The time at which we're currently rendering work.
205
let nextRenderExpirationTime: ExpirationTime = NoWork;
206
207
// The next fiber with an effect that we're currently committing.
208
let nextEffect: Fiber | null = null;
209
223
- let pendingCommit: Fiber | null = null;
224
-
225
- // Linked list of roots with scheduled work on them.
226
- let nextScheduledRoot: FiberRoot | null = null;
227
- let lastScheduledRoot: FiberRoot | null = null;
228
-
229
- // Keep track of which host environment callbacks are scheduled.
230
- let isCallbackScheduled: boolean = false;
231
-
210
// Keep track of which fibers have captured an error that need to be handled.
211
// Work is removed from this collection after componentDidCatch is called.
212
let capturedErrors: Map<Fiber, CapturedError> | null = null;
223
let isCommitting: boolean = false;
224
let isUnmounting: boolean = false;
225
248
- // Use these to prevent an infinite loop of nested updates
249
- const NESTED_UPDATE_LIMIT = 1000;
250
- let nestedUpdateCount: number = 0;
251
- let nextRenderedTree: FiberRoot | null = null;
252
-
226
function resetContextStack() {
227
// Reset the stack
228
reset();
231
resetHostContainer();
232
}
233
261
- function resetNextUnitOfWork() {
262
- // Clear out roots with no more work on them, or if they have uncaught errors
263
- while (
264
- nextScheduledRoot !== null &&
265
- nextScheduledRoot.current.expirationTime === NoWork
266
- ) {
267
- // Unschedule this root.
268
- nextScheduledRoot.isScheduled = false;
269
- // Read the next pointer now.
270
- // We need to clear it in case this root gets scheduled again later.
271
- const next = nextScheduledRoot.nextScheduledRoot;
272
- nextScheduledRoot.nextScheduledRoot = null;
273
- // Exit if we cleared all the roots and there's no work to do.
274
- if (nextScheduledRoot === lastScheduledRoot) {
275
- nextScheduledRoot = null;
276
- lastScheduledRoot = null;
277
- nextRenderExpirationTime = NoWork;
278
- return null;
279
- }
280
- // Continue with the next root.
281
- // If there's no work on it, it will get unscheduled too.
282
- nextScheduledRoot = next;
283
- }
284
-
285
- let root = nextScheduledRoot;
286
- let earliestExpirationRoot = null;
287
- let earliestExpirationTime = NoWork;
288
- while (root !== null) {
289
- if (
290
- root.current.expirationTime !== NoWork &&
291
- (earliestExpirationTime === NoWork ||
292
- earliestExpirationTime > root.current.expirationTime)
293
- ) {
294
- earliestExpirationTime = root.current.expirationTime;
295
- earliestExpirationRoot = root;
296
- }
297
- // We didn't find anything to do in this root, so let's try the next one.
298
- root = root.nextScheduledRoot;
299
- }
300
- if (earliestExpirationRoot !== null) {
301
- nextRenderExpirationTime = earliestExpirationTime;
302
- // Before we start any new work, let's make sure that we have a fresh
303
- // stack to work from.
304
- // TODO: This call is buried a bit too deep. It would be nice to have
305
- // a single point which happens right before any new work and
306
- // unfortunately this is it.
307
- resetContextStack();
308
-
309
- nextUnitOfWork = createWorkInProgress(
310
- earliestExpirationRoot.current,
311
- earliestExpirationTime,
312
- );
313
- if (earliestExpirationRoot !== nextRenderedTree) {
314
- // We've switched trees. Reset the nested update counter.
315
- nestedUpdateCount = 0;
316
- nextRenderedTree = earliestExpirationRoot;
317
- }
318
- return;
319
- }
320
-
321
- nextRenderExpirationTime = NoWork;
322
- nextUnitOfWork = null;
323
- nextRenderedTree = null;
324
- return;
325
- }
326
-
234
function commitAllHostEffects() {
235
while (nextEffect !== null) {
236
if (__DEV__) {
337
}
338
}
339
433
- function commitAllWork(finishedWork: Fiber) {
340
+ function commitRoot(finishedWork: Fiber): ExpirationTime {
341
// We keep track of this so that captureError can collect any boundaries
342
// that capture an error during the commit phase. The reason these aren't
343
// local to this function is because errors that occur during cWU are
344
// captured elsewhere, to prevent the unmount from being interrupted.
345
+ isWorking = true;
346
isCommitting = true;
347
if (__DEV__) {
348
startCommitTimer();
349
}
350
443
- pendingCommit = null;
444
- const root: FiberRoot = (finishedWork.stateNode: any);
351
+ const root: FiberRoot = finishedWork.stateNode;
352
invariant(
353
root.current !== finishedWork,
354
'Cannot commit the same tree as before. This is probably a bug ' +
355
'related to the return field. This error is likely caused by a bug ' +
356
'in React. Please file an issue.',
357
);
451
-
452
- if (nextRenderExpirationTime <= mostRecentCurrentTime) {
453
- // Keep track of the number of iterations to prevent an infinite
454
- // update loop.
455
- nestedUpdateCount++;
456
- }
358
+ root.isReadyForCommit = false;
359
360
// Reset this to null before calling lifecycles
361
ReactCurrentOwner.current = null;
467
}
468
469
isCommitting = false;
470
+ isWorking = false;
471
if (__DEV__) {
472
stopCommitLifeCyclesTimer();
473
stopCommitTimer();
486
commitPhaseBoundaries = null;
487
}
488
586
- // This tree is done. Reset the unit of work pointer to the root that
587
- // expires soonest. If there's no work left, the pointer is set to null.
588
- resetNextUnitOfWork();
489
+ if (firstUncaughtError !== null) {
490
+ const error = firstUncaughtError;
491
+ firstUncaughtError = null;
492
+ onUncaughtError(error);
493
+ }
494
+
495
+ const remainingTime = root.current.expirationTime;
496
+
497
+ if (remainingTime === NoWork) {
498
+ capturedErrors = null;
499
+ failedBoundaries = null;
500
+ }
501
+
502
+ return remainingTime;
503
}
504
505
function resetExpirationTime(
616
workInProgress = returnFiber;
617
continue;
618
} else {
705
- // We've reached the root. Mark the root as pending commit. Depending
706
- // on how much time we have left, we'll either commit it now or in
707
- // the next frame.
708
- pendingCommit = workInProgress;
619
+ // We've reached the root.
620
+ const root: FiberRoot = workInProgress.stateNode;
621
+ root.isReadyForCommit = true;
622
return null;
623
}
624
}
693
return next;
694
}
695
783
- function performDeferredWork(deadline: Deadline) {
784
- performWork(Never, deadline);
785
- }
696
+ function workLoop(expirationTime: ExpirationTime) {
697
+ if (capturedErrors !== null) {
698
+ // If there are unhandled errors, switch to the slow work loop.
699
+ // TODO: How to avoid this check in the fast path? Maybe the renderer
700
+ // could keep track of which roots have unhandled errors and call a
701
+ // forked version of renderRoot.
702
+ slowWorkLoopThatChecksForFailedWork(expirationTime);
703
+ return;
704
+ }
705
+ if (
706
+ nextRenderExpirationTime === NoWork ||
707
+ nextRenderExpirationTime > expirationTime
708
+ ) {
709
+ return;
710
+ }
711
787
- function handleCommitPhaseErrors() {
788
- // This is a special work loop for handling commit phase errors. It's
789
- // similar to the syncrhonous work loop, but does an additional check on
790
- // each fiber to see if it's an error boundary with an unhandled error. If
791
- // so, it uses a forked version of performUnitOfWork that unmounts the
792
- // failed subtree.
793
- //
794
- // The loop stops once the children have unmounted and error lifecycles are
795
- // called. Then we return to the regular flow.
712
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
713
+ // Flush all expired work.
714
+ while (nextUnitOfWork !== null) {
715
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
716
+ }
717
+ } else {
718
+ // Flush asynchronous work until the deadline runs out of time.
719
+ while (nextUnitOfWork !== null && !shouldYield()) {
720
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
721
+ }
722
+ }
723
+ }
724
725
+ function slowWorkLoopThatChecksForFailedWork(expirationTime: ExpirationTime) {
726
if (
798
- capturedErrors !== null &&
799
- capturedErrors.size > 0 &&
800
- nextRenderExpirationTime !== NoWork &&
801
- nextRenderExpirationTime <= mostRecentCurrentTime
727
+ nextRenderExpirationTime === NoWork ||
728
+ nextRenderExpirationTime > expirationTime
729
) {
730
+ return;
731
+ }
732
+
733
+ if (nextRenderExpirationTime <= mostRecentCurrentTime) {
734
+ // Flush all expired work.
735
while (nextUnitOfWork !== null) {
736
if (hasCapturedError(nextUnitOfWork)) {
737
// Use a forked version of performUnitOfWork
739
} else {
740
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
741
}
810
- if (nextUnitOfWork === null) {
811
- invariant(
812
- pendingCommit !== null,
813
- 'Should have a pending commit. This error is likely caused by ' +
814
- 'a bug in React. Please file an issue.',
815
- );
816
- // We just completed a root. Commit it now.
817
- commitAllWork(pendingCommit);
818
- if (
819
- capturedErrors === null ||
820
- capturedErrors.size === 0 ||
821
- nextRenderExpirationTime === NoWork ||
822
- nextRenderExpirationTime > mostRecentCurrentTime
823
- ) {
824
- // There are no more unhandled errors. We can exit this special
825
- // work loop. If there's still additional work, we'll perform it
826
- // using one of the normal work loops.
827
- break;
828
- }
829
- // The commit phase produced additional errors. Continue working.
830
- }
742
}
832
- }
833
- }
834
-
835
- function workLoop(
836
- minExpirationTime: ExpirationTime,
837
- deadline: Deadline | null,
838
- ) {
839
- loop: do {
840
- if (pendingCommit !== null) {
841
- commitAllWork(pendingCommit);
842
- handleCommitPhaseErrors();
843
- } else if (nextUnitOfWork === null) {
844
- resetNextUnitOfWork();
845
- }
846
-
847
- if (
848
- nextRenderExpirationTime === NoWork ||
849
- nextRenderExpirationTime > minExpirationTime
850
- ) {
851
- return;
852
- }
853
-
854
- if (nextRenderExpirationTime <= mostRecentCurrentTime) {
855
- // Flush all expired work.
856
- while (nextUnitOfWork !== null) {
743
+ } else {
744
+ // Flush asynchronous work until the deadline runs out of time.
745
+ while (nextUnitOfWork !== null && !shouldYield()) {
746
+ if (hasCapturedError(nextUnitOfWork)) {
747
+ // Use a forked version of performUnitOfWork
748
+ nextUnitOfWork = performFailedUnitOfWork(nextUnitOfWork);
749
+ } else {
750
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
858
- if (nextUnitOfWork === null) {
859
- invariant(
860
- pendingCommit !== null,
861
- 'Should have a pending commit. This error is likely caused by ' +
862
- 'a bug in React. Please file an issue.',
863
- );
864
- // We just completed a root. Commit it now.
865
- commitAllWork(pendingCommit);
866
- // Clear any errors that were scheduled during the commit phase.
867
- handleCommitPhaseErrors();
868
- // The render time may have changed. Check again.
869
- if (
870
- nextRenderExpirationTime === NoWork ||
871
- nextRenderExpirationTime > minExpirationTime ||
872
- nextRenderExpirationTime > mostRecentCurrentTime
873
- ) {
874
- // We've completed all the expired work.
875
- break;
876
- }
877
- }
878
- }
879
- } else if (deadline !== null) {
880
- // Flush asynchronous work until the deadline runs out of time.
881
- while (nextUnitOfWork !== null && !deadlineHasExpired) {
882
- if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
883
- nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
884
- // In a deferred work batch, iff nextUnitOfWork returns null, we just
885
- // completed a root and a pendingCommit exists. Logically, we could
886
- // omit either of the checks in the following condition, but we need
887
- // both to satisfy Flow.
888
- if (nextUnitOfWork === null) {
889
- invariant(
890
- pendingCommit !== null,
891
- 'Should have a pending commit. This error is likely caused by ' +
892
- 'a bug in React. Please file an issue.',
893
- );
894
- // We just completed a root. If we have time, commit it now.
895
- // Otherwise, we'll commit it in the next frame.
896
- if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
897
- commitAllWork(pendingCommit);
898
- // Clear any errors that were scheduled during the commit phase.
899
- handleCommitPhaseErrors();
900
- // The render time may have changed. Check again.
901
- if (
902
- nextRenderExpirationTime === NoWork ||
903
- nextRenderExpirationTime > minExpirationTime ||
904
- nextRenderExpirationTime <= mostRecentCurrentTime
905
- ) {
906
- // We've completed all the async work.
907
- break;
908
- }
909
- } else {
910
- deadlineHasExpired = true;
911
- }
912
- }
913
- } else {
914
- deadlineHasExpired = true;
915
- }
751
}
752
}
918
-
919
- // There might be work left. Depending on the priority, we should
920
- // either perform it now or schedule a callback to perform it later.
921
- const currentTime = recalculateCurrentTime();
922
- switch (relativeExpirationTime(currentTime, nextRenderExpirationTime)) {
923
- case NoWork:
924
- // No work left. We can exit.
925
- break loop;
926
- case Sync:
927
- case Task:
928
- // We have remaining synchronous or task work. Keep performing it,
929
- // regardless of whether we're inside a callback.
930
- if (nextRenderExpirationTime <= minExpirationTime) {
931
- // Sometimes minExpirationTime is Sync, which means we should skip
932
- // task work.
933
- continue loop;
934
- }
935
- break loop;
936
- default:
937
- // We have remaining async work.
938
- if (deadline === null) {
939
- // We're not inside a callback. Exit and perform the work during
940
- // the next callback.
941
- break loop;
942
- }
943
- // We are inside a callback.
944
- if (
945
- !deadlineHasExpired &&
946
- nextRenderExpirationTime <= minExpirationTime
947
- ) {
948
- // We still have time. Keep working.
949
- continue loop;
950
- }
951
- // We've run out of time. Exit.
952
- break loop;
953
- }
954
- } while (true);
753
+ }
754
}
755
957
- function performWorkCatchBlock(
756
+ function renderRootCatchBlock(
757
+ root: FiberRoot,
758
failedWork: Fiber,
759
boundary: Fiber,
960
- minExpirationTime: ExpirationTime,
961
- deadline: Deadline | null,
760
+ expirationTime: ExpirationTime,
761
) {
762
// We're going to restart the error boundary that captured the error.
763
// Conceptually, we're unwinding the stack. We need to unwind the
772
nextUnitOfWork = performFailedUnitOfWork(boundary);
773
774
// Continue working.
976
- workLoop(minExpirationTime, deadline);
775
+ workLoop(expirationTime);
776
}
777
979
- function performWork(
980
- minExpirationTime: ExpirationTime,
981
- deadline: Deadline | null,
982
- ) {
778
+ function renderRoot(
779
+ root: FiberRoot,
780
+ expirationTime: ExpirationTime,
781
+ ): Fiber | null {
782
if (__DEV__) {
783
startWorkLoopTimer();
784
}
785
786
invariant(
988
- !isPerformingWork,
989
- 'performWork was called recursively. This error is likely caused ' +
787
+ !isWorking,
788
+ 'renderRoot was called recursively. This error is likely caused ' +
789
'by a bug in React. Please file an issue.',
790
);
992
- isPerformingWork = true;
791
+ isWorking = true;
792
994
- nestedUpdateCount = 0;
793
+ // We're about to mutate the work-in-progress tree. If the root was pending
794
+ // commit, it no longer is: we'll need to complete it again.
795
+ root.isReadyForCommit = false;
796
+
797
+ // Check if we're starting from a fresh stack, or if we're resuming from
798
+ // previously yielded work.
799
+ if (
800
+ root !== nextRoot ||
801
+ expirationTime !== nextRenderExpirationTime ||
802
+ nextUnitOfWork === null
803
+ ) {
804
+ // This is a restart. Reset the stack.
805
+ resetContextStack();
806
+ nextRoot = root;
807
+ nextRenderExpirationTime = expirationTime;
808
+ nextUnitOfWork = createWorkInProgress(nextRoot.current, expirationTime);
809
+ }
810
811
let didError = false;
812
let error = null;
813
if (__DEV__) {
999
- invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline);
814
+ invokeGuardedCallback(null, workLoop, null, expirationTime);
815
if (hasCaughtError()) {
816
didError = true;
817
error = clearCaughtError();
818
}
819
} else {
820
try {
1006
- workLoop(minExpirationTime, deadline);
821
+ workLoop(expirationTime);
822
} catch (e) {
823
didError = true;
824
error = e;
861
if (__DEV__) {
862
invokeGuardedCallback(
863
null,
1049
- performWorkCatchBlock,
864
+ renderRootCatchBlock,
865
null,
866
+ root,
867
failedWork,
868
boundary,
1053
- minExpirationTime,
1054
- deadline,
869
+ expirationTime,
870
);
871
if (hasCaughtError()) {
872
didError = true;
875
}
876
} else {
877
try {
1063
- performWorkCatchBlock(
1064
- failedWork,
1065
- boundary,
1066
- minExpirationTime,
1067
- deadline,
1068
- );
878
+ renderRootCatchBlock(root, failedWork, boundary, expirationTime);
879
error = null;
880
} catch (e) {
881
didError = true;
887
break;
888
}
889
1080
- // If we're inside a callback, set this to false, since we just flushed it.
1081
- if (deadline !== null) {
1082
- isCallbackScheduled = false;
1083
- }
1084
- // If there's remaining async work, make sure we schedule another callback.
1085
- if (
1086
- nextRenderExpirationTime > mostRecentCurrentTime &&
1087
- !isCallbackScheduled
1088
- ) {
1089
- scheduleDeferredCallback(performDeferredWork);
1090
- isCallbackScheduled = true;
1091
- }
1092
-
1093
- const errorToThrow = firstUncaughtError;
890
+ const uncaughtError = firstUncaughtError;
891
892
// We're done performing work. Time to clean up.
1096
- isPerformingWork = false;
1097
- deadlineHasExpired = false;
893
+ isWorking = false;
894
didFatal = false;
895
firstUncaughtError = null;
1100
- capturedErrors = null;
1101
- failedBoundaries = null;
1102
- nextRenderedTree = null;
1103
- nestedUpdateCount = 0;
896
897
if (__DEV__) {
898
stopWorkLoopTimer();
899
}
900
1109
- // It's safe to throw any unhandled errors.
1110
- if (errorToThrow !== null) {
1111
- throw errorToThrow;
901
+ if (uncaughtError !== null) {
902
+ onUncaughtError(uncaughtError);
903
}
904
+
905
+ return root.isReadyForCommit ? root.current.alternate : null;
906
}
907
908
// Returns the boundary that captured the error, or null if the error is ignored
1103
return;
1104
case HostRoot:
1105
if (firstUncaughtError === null) {
1313
- // If this is the host container, we treat it as a no-op error
1314
- // boundary. We'll throw the first uncaught error once it's safe to
1315
- // do so, at the end of the batch.
1106
firstUncaughtError = capturedError.error;
1107
}
1108
return;
1144
}
1145
}
1146
1357
- function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1358
- if (expirationTime === NoWork) {
1359
- return;
1360
- }
1361
-
1362
- if (!root.isScheduled) {
1363
- root.isScheduled = true;
1364
- if (lastScheduledRoot) {
1365
- // Schedule ourselves to the end.
1366
- lastScheduledRoot.nextScheduledRoot = root;
1367
- lastScheduledRoot = root;
1368
- } else {
1369
- // We're the only work scheduled.
1370
- nextScheduledRoot = root;
1371
- lastScheduledRoot = root;
1372
- }
1373
- }
1374
- }
1375
-
1147
function computeAsyncExpiration() {
1148
// Given the current clock time, returns an expiration time. We use rounding
1149
// to batch like updates together.
1159
if (expirationContext !== NoWork) {
1160
// An explicit expiration context was set;
1161
expirationTime = expirationContext;
1391
- } else if (isPerformingWork) {
1162
+ } else if (isWorking) {
1163
if (isCommitting) {
1164
// Updates that occur during the commit phase should have sync priority
1165
// by default.
1183
1184
if (
1185
expirationTime === Sync &&
1415
- (isBatchingUpdates || (isUnbatchingUpdates && isCommitting))
1186
+ isBatchingUpdates &&
1187
+ (!isUnbatchingUpdates || isCommitting)
1188
) {
1189
// If we're in a batch, downgrade sync to task.
1190
expirationTime = Task;
1205
recordScheduleUpdate();
1206
}
1207
1436
- if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1437
- didFatal = true;
1438
- invariant(
1439
- false,
1440
- 'Maximum update depth exceeded. This can happen when a ' +
1441
- 'component repeatedly calls setState inside componentWillUpdate or ' +
1442
- 'componentDidUpdate. React limits the number of nested updates to ' +
1443
- 'prevent infinite loops.',
1444
- );
1445
- }
1446
-
1447
- if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) {
1448
- // We must reset the current unit of work pointer so that we restart the
1449
- // search from the root during the next tick, in case there is now higher
1450
- // priority work somewhere earlier than before.
1451
- nextUnitOfWork = null;
1452
- }
1453
-
1208
if (__DEV__) {
1209
if (!isErrorRecovery && fiber.tag === ClassComponent) {
1210
const instance = fiber.stateNode;
1213
}
1214
1215
let node = fiber;
1462
- let shouldContinue = true;
1463
- while (node !== null && shouldContinue) {
1464
- // Walk the parent path to the root and update each node's expiration
1465
- // time. Once we reach a node whose expiration matches (and whose
1466
- // alternate's expiration matches) we can exit safely knowing that the
1467
- // rest of the path is correct.
1468
- shouldContinue = false;
1216
+ while (node !== null) {
1217
+ // Walk the parent path to the root and update each node's
1218
+ // expiration time.
1219
if (
1220
node.expirationTime === NoWork ||
1221
node.expirationTime > expirationTime
1222
) {
1473
- // Expiration time did not match. Update and keep going.
1474
- shouldContinue = true;
1223
node.expirationTime = expirationTime;
1224
}
1225
if (node.alternate !== null) {
1227
node.alternate.expirationTime === NoWork ||
1228
node.alternate.expirationTime > expirationTime
1229
) {
1482
- // Expiration time did not match. Update and keep going.
1483
- shouldContinue = true;
1230
node.alternate.expirationTime = expirationTime;
1231
}
1232
}
1233
if (node.return === null) {
1234
if (node.tag === HostRoot) {
1235
const root: FiberRoot = (node.stateNode: any);
1490
- scheduleRoot(root, expirationTime);
1491
- if (!isPerformingWork) {
1492
- switch (expirationTime) {
1493
- case Sync:
1494
- if (isUnbatchingUpdates) {
1495
- // We're inside unbatchedUpdates, which is inside either
1496
- // batchedUpdates or a lifecycle. We should only flush
1497
- // synchronous work, not task work.
1498
- performWork(Sync, null);
1499
- } else {
1500
- // Flush both synchronous and task work.
1501
- performWork(Task, null);
1502
- }
1503
- break;
1504
- case Task:
1505
- invariant(
1506
- isBatchingUpdates,
1507
- 'Task updates can only be scheduled as a nested update or ' +
1508
- 'inside batchedUpdates. This error is likely caused by a ' +
1509
- 'bug in React. Please file an issue.',
1510
- );
1511
- break;
1512
- default:
1513
- // This update is async. Schedule a callback.
1514
- if (!isCallbackScheduled) {
1515
- scheduleDeferredCallback(performDeferredWork);
1516
- isCallbackScheduled = true;
1517
- }
1518
- }
1236
+ if (
1237
+ !isWorking &&
1238
+ root === nextRoot &&
1239
+ expirationTime <= nextRenderExpirationTime
1240
+ ) {
1241
+ // This is an interruption. Restart the root from the top.
1242
+ nextRoot = null;
1243
+ nextUnitOfWork = null;
1244
+ nextRenderExpirationTime = NoWork;
1245
}
1246
+ requestWork(root, expirationTime);
1247
} else {
1248
if (__DEV__) {
1249
if (!isErrorRecovery && fiber.tag === ClassComponent) {
1268
return mostRecentCurrentTime;
1269
}
1270
1271
+ function deferredUpdates<A>(fn: () => A): A {
1272
+ const previousExpirationContext = expirationContext;
1273
+ expirationContext = computeAsyncExpiration();
1274
+ try {
1275
+ return fn();
1276
+ } finally {
1277
+ expirationContext = previousExpirationContext;
1278
+ }
1279
+ }
1280
+
1281
+ function syncUpdates<A>(fn: () => A): A {
1282
+ const previousExpirationContext = expirationContext;
1283
+ expirationContext = Sync;
1284
+ try {
1285
+ return fn();
1286
+ } finally {
1287
+ expirationContext = previousExpirationContext;
1288
+ }
1289
+ }
1290
+
1291
+ // TODO: Everything below this is written as if it has been lifted to the
1292
+ // renderers. I'll do this in a follow-up.
1293
+
1294
+ // Linked-list of roots
1295
+ let firstScheduledRoot: FiberRoot | null = null;
1296
+ let lastScheduledRoot: FiberRoot | null = null;
1297
+
1298
+ let isCallbackScheduled: boolean = false;
1299
+ let isRendering: boolean = false;
1300
+ let nextFlushedRoot: FiberRoot | null = null;
1301
+ let nextFlushedExpirationTime: ExpirationTime = NoWork;
1302
+ let deadlineDidExpire: boolean = false;
1303
+ let hasUnhandledError: boolean = false;
1304
+ let unhandledError: mixed | null = null;
1305
+ let deadline: Deadline | null = null;
1306
+
1307
+ let isBatchingUpdates: boolean = false;
1308
+ let isUnbatchingUpdates: boolean = false;
1309
+
1310
+ // Use these to prevent an infinite loop of nested updates
1311
+ const NESTED_UPDATE_LIMIT = 1000;
1312
+ let nestedUpdateCount: number = 0;
1313
+
1314
+ const timeHeuristicForUnitOfWork = 1;
1315
+
1316
+ // requestWork is called by the scheduler whenever a root receives an update.
1317
+ // It's up to the renderer to call renderRoot at some point in the future.
1318
+ function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
1319
+ if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1320
+ invariant(
1321
+ false,
1322
+ 'Maximum update depth exceeded. This can happen when a ' +
1323
+ 'component repeatedly calls setState inside componentWillUpdate or ' +
1324
+ 'componentDidUpdate. React limits the number of nested updates to ' +
1325
+ 'prevent infinite loops.',
1326
+ );
1327
+ }
1328
+
1329
+ // Check if this root is already part of the schedule.
1330
+ if (root.remainingExpirationTime === NoWork) {
1331
+ // This root is not already scheduled. Add it.
1332
+ root.remainingExpirationTime = expirationTime;
1333
+ if (lastScheduledRoot === null) {
1334
+ firstScheduledRoot = lastScheduledRoot = root;
1335
+ } else {
1336
+ lastScheduledRoot.nextScheduledRoot = root;
1337
+ lastScheduledRoot = root;
1338
+ }
1339
+ } else {
1340
+ // This root is already scheduled, but its priority may have increased.
1341
+ const remainingExpirationTime = root.remainingExpirationTime;
1342
+ if (
1343
+ remainingExpirationTime === NoWork ||
1344
+ expirationTime < remainingExpirationTime
1345
+ ) {
1346
+ // Update the priority.
1347
+ root.remainingExpirationTime = expirationTime;
1348
+ }
1349
+ }
1350
+
1351
+ // If we're not already rendering, schedule work to flush now (if it's
1352
+ // sync) or later (if it's async).
1353
+ if (!isRendering) {
1354
+ // TODO: Remove distinction between sync and task. Maybe we can remove
1355
+ // these magic numbers entirely by always comparing to the current time?
1356
+ if (expirationTime === Sync) {
1357
+ if (isUnbatchingUpdates) {
1358
+ performWork(Sync, null);
1359
+ } else {
1360
+ performWork(Task, null);
1361
+ }
1362
+ } else if (!isCallbackScheduled) {
1363
+ isCallbackScheduled = true;
1364
+ scheduleDeferredCallback(flushAsyncWork);
1365
+ }
1366
+ }
1367
+ }
1368
+
1369
+ function findHighestPriorityRoot() {
1370
+ let highestPriorityWork = NoWork;
1371
+ let highestPriorityRoot = null;
1372
+
1373
+ let previousScheduledRoot = null;
1374
+ let root = firstScheduledRoot;
1375
+ while (root !== null) {
1376
+ const remainingExpirationTime = root.remainingExpirationTime;
1377
+ if (remainingExpirationTime === NoWork) {
1378
+ // If this root no longer has work, remove it from the scheduler.
1379
+ let next = root.nextScheduledRoot;
1380
+ root.nextScheduledRoot = null;
1381
+ if (previousScheduledRoot === null) {
1382
+ firstScheduledRoot = next;
1383
+ } else {
1384
+ previousScheduledRoot.nextScheduledRoot = next;
1385
+ }
1386
+ if (next === null) {
1387
+ lastScheduledRoot = null;
1388
+ }
1389
+ root = next;
1390
+ continue;
1391
+ } else if (
1392
+ highestPriorityWork === NoWork ||
1393
+ remainingExpirationTime < highestPriorityWork
1394
+ ) {
1395
+ // Update the priority, if it's higher
1396
+ highestPriorityWork = remainingExpirationTime;
1397
+ highestPriorityRoot = root;
1398
+ }
1399
+ previousScheduledRoot = root;
1400
+ root = root.nextScheduledRoot;
1401
+ }
1402
+
1403
+ // If the next root is the same as the previous root, this is a nested
1404
+ // update. To prevent an infinite loop, increment the nested update count.
1405
+ const previousFlushedRoot = nextFlushedRoot;
1406
+ if (
1407
+ previousFlushedRoot !== null &&
1408
+ previousFlushedRoot === highestPriorityRoot
1409
+ ) {
1410
+ nestedUpdateCount++;
1411
+ } else {
1412
+ // Reset whenever we switch roots.
1413
+ nestedUpdateCount = 0;
1414
+ }
1415
+ nextFlushedRoot = highestPriorityRoot;
1416
+ nextFlushedExpirationTime = highestPriorityWork;
1417
+ }
1418
+
1419
+ function flushAsyncWork(dl) {
1420
+ performWork(NoWork, dl);
1421
+ }
1422
+
1423
+ function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1424
+ invariant(
1425
+ !isRendering,
1426
+ 'performWork was called recursively. This error is likely caused ' +
1427
+ 'by a bug in React. Please file an issue.',
1428
+ );
1429
+
1430
+ isRendering = true;
1431
+ deadline = dl;
1432
+
1433
+ // Keep working on roots until there's no more work, or until the we reach
1434
+ // the deadlne.
1435
+ findHighestPriorityRoot();
1436
+ while (
1437
+ nextFlushedRoot !== null &&
1438
+ nextFlushedExpirationTime !== NoWork &&
1439
+ (minExpirationTime === NoWork ||
1440
+ nextFlushedExpirationTime <= minExpirationTime) &&
1441
+ !deadlineDidExpire
1442
+ ) {
1443
+ // Check if this is async work or sync/expired work.
1444
+ // TODO: Pass current time as argument to renderRoot, commitRoot
1445
+ if (nextFlushedExpirationTime <= recalculateCurrentTime()) {
1446
+ // Flush sync work.
1447
+ let finishedWork = nextFlushedRoot.finishedWork;
1448
+ if (finishedWork !== null) {
1449
+ // This root is already complete. We can commit it.
1450
+ nextFlushedRoot.finishedWork = null;
1451
+ nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1452
+ } else {
1453
+ nextFlushedRoot.finishedWork = null;
1454
+ finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
1455
+ if (finishedWork !== null) {
1456
+ // We've completed the root. Commit it.
1457
+ nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1458
+ }
1459
+ }
1460
+ } else {
1461
+ // Flush async work.
1462
+ let finishedWork = nextFlushedRoot.finishedWork;
1463
+ if (finishedWork !== null) {
1464
+ // This root is already complete. We can commit it.
1465
+ nextFlushedRoot.finishedWork = null;
1466
+ nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
1467
+ } else {
1468
+ nextFlushedRoot.finishedWork = null;
1469
+ finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
1470
+ if (finishedWork !== null) {
1471
+ // We've completed the root. Check the deadline one more time
1472
+ // before committing.
1473
+ if (!shouldYield()) {
1474
+ // Still time left. Commit the root.
1475
+ nextFlushedRoot.remainingExpirationTime = commitRoot(
1476
+ finishedWork,
1477
+ );
1478
+ } else {
1479
+ // There's no time left. Mark this root as complete. We'll come
1480
+ // back and commit it later.
1481
+ nextFlushedRoot.finishedWork = finishedWork;
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+ // Find the next highest priority work.
1487
+ findHighestPriorityRoot();
1488
+ }
1489
+
1490
+ // We're done flushing work. Either we ran out of time in this callback,
1491
+ // or there's no more work left with sufficient priority.
1492
+
1493
+ // If we're inside a callback, set this to false since we just completed it.
1494
+ if (deadline !== null) {
1495
+ isCallbackScheduled = false;
1496
+ }
1497
+ // If there's work left over, schedule a new callback.
1498
+ if (nextFlushedRoot !== null && !isCallbackScheduled) {
1499
+ isCallbackScheduled = true;
1500
+ scheduleDeferredCallback(flushAsyncWork);
1501
+ }
1502
+
1503
+ // Clean-up.
1504
+ deadline = null;
1505
+ deadlineDidExpire = false;
1506
+ isRendering = false;
1507
+ nestedUpdateCount = 0;
1508
+
1509
+ if (hasUnhandledError) {
1510
+ const error = unhandledError;
1511
+ unhandledError = null;
1512
+ hasUnhandledError = false;
1513
+ throw error;
1514
+ }
1515
+ }
1516
+
1517
+ // When working on async work, the reconciler asks the renderer if it should
1518
+ // yield execution. For DOM, we implement this with requestIdleCallback.
1519
+ function shouldYield() {
1520
+ if (deadline === null) {
1521
+ return false;
1522
+ }
1523
+ if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
1524
+ return false;
1525
+ }
1526
+ deadlineDidExpire = true;
1527
+ return true;
1528
+ }
1529
+
1530
+ // TODO: Not happy about this hook. Conceptually, renderRoot should return a
1531
+ // tuple of (isReadyForCommit, didError, error)
1532
+ function onUncaughtError(error) {
1533
+ invariant(
1534
+ nextFlushedRoot !== null,
1535
+ 'Should be working on a root. This error is likely caused by a bug in ' +
1536
+ 'React. Please file an issue.',
1537
+ );
1538
+ // Unschedule this root so we don't work on it again until there's
1539
+ // another update.
1540
+ nextFlushedRoot.remainingExpirationTime = NoWork;
1541
+ if (!hasUnhandledError) {
1542
+ hasUnhandledError = true;
1543
+ unhandledError = error;
1544
+ }
1545
+ }
1546
+
1547
+ // TODO: Batching should be implemented at the renderer level, not inside
1548
+ // the reconciler.
1549
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1550
const previousIsBatchingUpdates = isBatchingUpdates;
1551
isBatchingUpdates = true;
1553
return fn(a);
1554
} finally {
1555
isBatchingUpdates = previousIsBatchingUpdates;
1551
- // If we're not already inside a batch, we need to flush any task work
1552
- // that was created by the user-provided function.
1553
- if (!isPerformingWork && !isBatchingUpdates) {
1556
+ if (!isBatchingUpdates && !isRendering) {
1557
performWork(Task, null);
1558
}
1559
}
1560
}
1561
1562
+ // TODO: Batching should be implemented at the renderer level, not inside
1563
+ // the reconciler.
1564
function unbatchedUpdates<A>(fn: () => A): A {
1560
- const previousIsUnbatchingUpdates = isUnbatchingUpdates;
1561
- const previousIsBatchingUpdates = isBatchingUpdates;
1562
- // This is only true if we're nested inside batchedUpdates.
1563
- isUnbatchingUpdates = isBatchingUpdates;
1564
- isBatchingUpdates = false;
1565
- try {
1566
- return fn();
1567
- } finally {
1568
- isBatchingUpdates = previousIsBatchingUpdates;
1569
- isUnbatchingUpdates = previousIsUnbatchingUpdates;
1565
+ if (isBatchingUpdates && !isUnbatchingUpdates) {
1566
+ isUnbatchingUpdates = true;
1567
+ try {
1568
+ return fn();
1569
+ } finally {
1570
+ isUnbatchingUpdates = false;
1571
+ }
1572
}
1573
+ return fn();
1574
}
1575
1573
- function flushSync<A>(batch: () => A): A {
1576
+ // TODO: Batching should be implemented at the renderer level, not within
1577
+ // the reconciler.
1578
+ function flushSync<A>(fn: () => A): A {
1579
const previousIsBatchingUpdates = isBatchingUpdates;
1575
- const previousExpirationContext = expirationContext;
1580
isBatchingUpdates = true;
1577
- expirationContext = Sync;
1581
try {
1579
- return batch();
1582
+ return syncUpdates(fn);
1583
} finally {
1584
isBatchingUpdates = previousIsBatchingUpdates;
1582
- expirationContext = previousExpirationContext;
1583
-
1585
invariant(
1585
- !isPerformingWork,
1586
+ !isRendering,
1587
'flushSync was called from inside a lifecycle method. It cannot be ' +
1588
'called when React is already rendering.',
1589
);
1591
}
1592
}
1593
1593
- function deferredUpdates<A>(fn: () => A): A {
1594
- const previousExpirationContext = expirationContext;
1595
- expirationContext = computeAsyncExpiration();
1596
- try {
1597
- return fn();
1598
- } finally {
1599
- expirationContext = previousExpirationContext;
1600
- }
1601
- }
1602
-
1594
return {
1604
- computeAsyncExpiration: computeAsyncExpiration,
1605
- computeExpirationForFiber: computeExpirationForFiber,
1606
- scheduleWork: scheduleWork,
1607
- batchedUpdates: batchedUpdates,
1608
- unbatchedUpdates: unbatchedUpdates,
1609
- flushSync: flushSync,
1610
- deferredUpdates: deferredUpdates,
1595
+ computeAsyncExpiration,
1596
+ computeExpirationForFiber,
1597
+ scheduleWork,
1598
+ batchedUpdates,
1599
+ unbatchedUpdates,
1600
+ flushSync,
1601
+ deferredUpdates,
1602
};
1603
};