60
import {
61
markPendingPriorityLevel,
62
markCommittedPriorityLevels,
63
- findNextPendingPriorityLevel,
63
markSuspendedPriorityLevel,
64
markPingedPriorityLevel,
65
} from './ReactFiberPendingPriority';
229
// The time at which we're currently rendering work.
230
let nextRenderExpirationTime: ExpirationTime = NoWork;
231
let nextLatestTimeoutMs: number = -1;
233
-let nextRenderIsExpired: boolean = false;
232
233
// The next fiber with an effect that we're currently committing.
234
let nextEffect: Fiber | null = null;
252
replayUnitOfWork = (
253
failedUnitOfWork: Fiber,
254
thrownValue: mixed,
257
- isAsync: boolean,
255
+ isYieldy: boolean,
256
) => {
257
if (
258
thrownValue !== null &&
301
// Replay the begin phase.
302
isReplayingFailedUnitOfWork = true;
303
originalReplayError = thrownValue;
306
- invokeGuardedCallback(null, workLoop, null, isAsync);
304
+ invokeGuardedCallback(null, workLoop, null, isYieldy);
305
isReplayingFailedUnitOfWork = false;
306
originalReplayError = null;
307
if (hasCaughtError()) {
343
nextRoot = null;
344
nextRenderExpirationTime = NoWork;
345
nextLatestTimeoutMs = -1;
348
- nextRenderIsExpired = false;
346
nextUnitOfWork = null;
347
}
348
491
}
492
}
493
497
-function commitRoot(finishedWork: Fiber): ExpirationTime {
494
+function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
495
isWorking = true;
496
isCommitting = true;
497
startCommitTimer();
498
502
- const root: FiberRoot = finishedWork.stateNode;
499
invariant(
500
root.current !== finishedWork,
501
'Cannot commit the same tree as before. This is probably a bug ' +
686
ReactFiberInstrumentation.debugTool.onCommitWork(finishedWork);
687
}
688
693
- const nextPendingPriorityLevel = findNextPendingPriorityLevel(root);
694
- if (nextPendingPriorityLevel === NoWork) {
689
+ const expirationTime = root.expirationTime;
690
+ if (expirationTime === NoWork) {
691
// If there's no remaining work, we can clear the set of already failed
692
// error boundaries.
693
legacyErrorBoundariesThatAlreadyFailed = null;
694
}
699
- return nextPendingPriorityLevel;
695
+ onCommit(root, expirationTime);
696
}
697
698
function resetExpirationTime(
849
// This fiber did not complete because something threw. Pop values off
850
// the stack without entering the complete phase. If this is a boundary,
851
// capture values if possible.
856
- const next = unwindWork(
857
- workInProgress,
858
- nextRenderIsExpired,
859
- nextRenderExpirationTime,
860
- );
852
+ const next = unwindWork(workInProgress, nextRenderExpirationTime);
853
// Because this fiber did not complete, don't reset its expiration time.
854
if (workInProgress.effectTag & DidCapture) {
855
// Restarting an error boundary
965
return next;
966
}
967
976
-function workLoop(isAsync) {
977
- if (!isAsync) {
978
- // Flush all expired work.
968
+function workLoop(isYieldy) {
969
+ if (!isYieldy) {
970
+ // Flush work without yielding
971
while (nextUnitOfWork !== null) {
972
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
973
}
985
}
986
}
987
996
-function renderRoot(
997
- root: FiberRoot,
998
- expirationTime: ExpirationTime,
999
- isAsync: boolean,
1000
-): Fiber | null {
988
+function renderRoot(root: FiberRoot, isYieldy: boolean): void {
989
invariant(
990
!isWorking,
991
'renderRoot was called recursively. This error is likely caused ' +
993
);
994
isWorking = true;
995
996
+ const expirationTime = root.nextExpirationTimeToWorkOn;
997
+
998
// Check if we're starting from a fresh stack, or if we're resuming from
999
// previously yielded work.
1000
if (
1017
1018
let didFatal = false;
1019
1030
- nextRenderIsExpired =
1031
- !isAsync || nextRenderExpirationTime <= mostRecentCurrentTime;
1032
-
1020
startWorkLoopTimer(nextUnitOfWork);
1021
1022
do {
1023
try {
1037
- workLoop(isAsync);
1024
+ workLoop(isYieldy);
1025
} catch (thrownValue) {
1026
if (enableProfilerTimer) {
1027
// Stop "base" render timer in the event of an error.
1041
1042
const failedUnitOfWork: Fiber = nextUnitOfWork;
1043
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
1057
- replayUnitOfWork(failedUnitOfWork, thrownValue, isAsync);
1044
+ replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
1045
}
1046
1047
// TODO: we already know this isn't true in some cases.
1072
returnFiber,
1073
sourceFiber,
1074
thrownValue,
1088
- nextRenderIsExpired,
1075
nextRenderExpirationTime,
1076
mostRecentCurrentTimeMs,
1077
);
1093
if (__DEV__) {
1094
resetStackAfterFatalErrorInDev();
1095
}
1110
- return null;
1096
+ onFatal(root);
1097
} else if (nextUnitOfWork === null) {
1098
// We reached the root.
1099
const rootWorkInProgress = root.current.alternate;
1107
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1108
interruptedBy = null;
1109
// The root successfully completed.
1124
- root.pendingCommitExpirationTime = expirationTime;
1125
- // Return the completed work-in-progress.
1126
- return rootWorkInProgress;
1110
+ onComplete(root, rootWorkInProgress, expirationTime);
1111
} else {
1112
// The root did not complete.
1113
const didCompleteRoot = false;
1114
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1115
interruptedBy = null;
1132
- if (nextLatestTimeoutMs >= 0) {
1133
- setTimeout(() => {
1134
- retrySuspendedRoot(root, expirationTime);
1135
- }, nextLatestTimeoutMs);
1136
- }
1116
markSuspendedPriorityLevel(root, expirationTime);
1138
- const firstUnblockedExpirationTime = findNextPendingPriorityLevel(root);
1139
- onBlock(firstUnblockedExpirationTime);
1140
- // Return null to indicate that the root did not complete.
1141
- return null;
1117
+ const suspendedExpirationTime = expirationTime;
1118
+ const newExpirationTime = root.expirationTime;
1119
+ onSuspend(
1120
+ root,
1121
+ suspendedExpirationTime,
1122
+ newExpirationTime,
1123
+ nextLatestTimeoutMs,
1124
+ );
1125
}
1126
} else {
1127
const didCompleteRoot = false;
1128
stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1129
interruptedBy = null;
1147
- // There's more work to do, but we ran out of time. Yield back to
1148
- // the renderer.
1149
- return null;
1130
+ onYield(root);
1131
}
1132
}
1133
1292
1293
function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) {
1294
markPingedPriorityLevel(root, suspendedTime);
1314
- const retryTime = findNextPendingPriorityLevel(root);
1295
+ const retryTime = root.expirationTime;
1296
if (retryTime !== NoWork) {
1316
- requestRetry(root, retryTime);
1297
+ requestWork(root, retryTime);
1298
}
1299
}
1300
1339
resetStack();
1340
}
1341
markPendingPriorityLevel(root, expirationTime);
1361
- const nextExpirationTimeToWorkOn = findNextPendingPriorityLevel(root);
1342
if (
1343
// If we're in the render phase, we don't need to schedule this root
1344
// for an update, because we'll do it before we exit...
1347
// ...unless this is a different root than the one we're rendering.
1348
nextRoot !== root
1349
) {
1370
- requestWork(root, nextExpirationTimeToWorkOn);
1350
+ const rootExpirationTime = root.expirationTime;
1351
+ requestWork(root, rootExpirationTime);
1352
}
1353
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
1354
invariant(
1435
1436
const timeHeuristicForUnitOfWork = 1;
1437
1457
-function scheduleCallbackWithExpiration(expirationTime) {
1438
+function scheduleCallbackWithExpirationTime(expirationTime) {
1439
if (callbackExpirationTime !== NoWork) {
1440
// A callback is already scheduled. Check its expiration time (timeout).
1441
if (expirationTime > callbackExpirationTime) {
1453
startRequestCallbackTimer();
1454
}
1455
1475
- // Compute a timeout for the given expiration time.
1476
- const currentMs = now() - originalStartTimeMs;
1477
- const expirationMs = expirationTimeToMs(expirationTime);
1478
- const timeout = expirationMs - currentMs;
1479
-
1456
callbackExpirationTime = expirationTime;
1457
+ const currentMs = now() - originalStartTimeMs;
1458
+ const expirationTimeMs = expirationTimeToMs(expirationTime);
1459
+ const timeout = expirationTimeMs - currentMs;
1460
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
1461
}
1462
1484
-function requestRetry(root: FiberRoot, expirationTime: ExpirationTime) {
1485
- if (
1486
- root.remainingExpirationTime === NoWork ||
1487
- root.remainingExpirationTime < expirationTime
1488
- ) {
1489
- // For a retry, only update the remaining expiration time if it has a
1490
- // *lower priority* than the existing value. This is because, on a retry,
1491
- // we should attempt to coalesce as much as possible.
1492
- requestWork(root, expirationTime);
1463
+// For every call to renderRoot, one of onFatal, onComplete, onSuspend, and
1464
+// onYield is called upon exiting. We use these in lieu of returning a tuple.
1465
+// I've also chosen not to inline them into renderRoot because these will
1466
+// eventually be lifted into the renderer.
1467
+function onFatal(root) {
1468
+ root.finishedWork = null;
1469
+}
1470
+
1471
+function onComplete(
1472
+ root: FiberRoot,
1473
+ finishedWork: Fiber,
1474
+ expirationTime: ExpirationTime,
1475
+) {
1476
+ root.pendingCommitExpirationTime = expirationTime;
1477
+ root.finishedWork = finishedWork;
1478
+}
1479
+
1480
+function onSuspend(
1481
+ root: FiberRoot,
1482
+ suspendedExpirationTime: ExpirationTime,
1483
+ expirationTime: ExpirationTime,
1484
+ timeoutMs: number,
1485
+) {
1486
+ if (timeoutMs >= 0) {
1487
+ const suspendedRoot = root;
1488
+ setTimeout(() => {
1489
+ retrySuspendedRoot(suspendedRoot, suspendedExpirationTime);
1490
+ }, timeoutMs);
1491
}
1492
+ root.expirationTime = expirationTime;
1493
+}
1494
+
1495
+function onYield(root) {
1496
+ root.finishedWork = null;
1497
+}
1498
+
1499
+function onCommit(root, expirationTime) {
1500
+ root.expirationTime = expirationTime;
1501
+ root.finishedWork = null;
1502
}
1503
1504
// requestWork is called by the scheduler whenever a root receives an update.
1528
if (expirationTime === Sync) {
1529
performSyncWork();
1530
} else {
1523
- scheduleCallbackWithExpiration(expirationTime);
1531
+ scheduleCallbackWithExpirationTime(expirationTime);
1532
}
1533
}
1534
1537
// Check if this root is already part of the schedule.
1538
if (root.nextScheduledRoot === null) {
1539
// This root is not already scheduled. Add it.
1532
- root.remainingExpirationTime = expirationTime;
1540
+ root.expirationTime = expirationTime;
1541
if (lastScheduledRoot === null) {
1542
firstScheduledRoot = lastScheduledRoot = root;
1543
root.nextScheduledRoot = root;
1548
}
1549
} else {
1550
// This root is already scheduled, but its priority may have increased.
1543
- const remainingExpirationTime = root.remainingExpirationTime;
1551
+ const remainingExpirationTime = root.expirationTime;
1552
if (
1553
remainingExpirationTime === NoWork ||
1554
expirationTime < remainingExpirationTime
1555
) {
1556
// Update the priority.
1549
- root.remainingExpirationTime = expirationTime;
1557
+ root.expirationTime = expirationTime;
1558
}
1559
}
1560
}
1566
let previousScheduledRoot = lastScheduledRoot;
1567
let root = firstScheduledRoot;
1568
while (root !== null) {
1561
- const remainingExpirationTime = root.remainingExpirationTime;
1569
+ const remainingExpirationTime = root.expirationTime;
1570
if (remainingExpirationTime === NoWork) {
1571
// This root no longer has work. Remove it from the scheduler.
1572
1636
}
1637
1638
function performAsyncWork(dl) {
1631
- performWork(NoWork, true, dl);
1639
+ performWork(NoWork, dl);
1640
}
1641
1642
function performSyncWork() {
1635
- performWork(Sync, false, null);
1643
+ performWork(Sync, null);
1644
}
1645
1638
-function performWork(
1639
- minExpirationTime: ExpirationTime,
1640
- isAsync: boolean,
1641
- dl: Deadline | null,
1642
-) {
1646
+function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
1647
deadline = dl;
1648
1649
// Keep working on roots until there's no more work, or until the we reach
1660
stopRequestCallbackTimer(didExpire, timeout);
1661
}
1662
1659
- if (isAsync) {
1663
+ if (deadline !== null) {
1664
while (
1665
nextFlushedRoot !== null &&
1666
nextFlushedExpirationTime !== NoWork &&
1670
recalculateCurrentTime() >= nextFlushedExpirationTime)
1671
) {
1672
recalculateCurrentTime();
1669
- performWorkOnRoot(
1670
- nextFlushedRoot,
1671
- nextFlushedExpirationTime,
1672
- !deadlineDidExpire,
1673
- );
1673
+ performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
1674
findHighestPriorityRoot();
1675
}
1676
} else {
1695
}
1696
// If there's work left over, schedule a new callback.
1697
if (nextFlushedExpirationTime !== NoWork) {
1698
- scheduleCallbackWithExpiration(nextFlushedExpirationTime);
1698
+ scheduleCallbackWithExpirationTime(nextFlushedExpirationTime);
1699
}
1700
1701
// Clean-up.
1752
function performWorkOnRoot(
1753
root: FiberRoot,
1754
expirationTime: ExpirationTime,
1755
- isAsync: boolean,
1755
+ isYieldy: boolean,
1756
) {
1757
invariant(
1758
!isRendering,
1763
isRendering = true;
1764
1765
// Check if this is async work or sync/expired work.
1766
- if (!isAsync) {
1767
- // Flush sync work.
1766
+ if (!isYieldy) {
1767
+ // Flush work without yielding.
1768
let finishedWork = root.finishedWork;
1769
if (finishedWork !== null) {
1770
// This root is already complete. We can commit it.
1771
completeRoot(root, finishedWork, expirationTime);
1772
} else {
1773
- finishedWork = renderRoot(root, expirationTime, false);
1773
+ root.finishedWork = null;
1774
+ renderRoot(root, false);
1775
+ finishedWork = root.finishedWork;
1776
if (finishedWork !== null) {
1777
// We've completed the root. Commit it.
1778
completeRoot(root, finishedWork, expirationTime);
1785
// This root is already complete. We can commit it.
1786
completeRoot(root, finishedWork, expirationTime);
1787
} else {
1786
- finishedWork = renderRoot(root, expirationTime, true);
1788
+ root.finishedWork = null;
1789
+ renderRoot(root, true);
1790
+ finishedWork = root.finishedWork;
1791
if (finishedWork !== null) {
1792
// We've completed the root. Check the deadline one more time
1793
// before committing.
1829
// This root is blocked from committing by a batch. Unschedule it until
1830
// we receive another update.
1831
root.finishedWork = finishedWork;
1828
- root.remainingExpirationTime = NoWork;
1832
+ root.expirationTime = NoWork;
1833
return;
1834
}
1835
}
1836
1837
// Commit the root.
1838
root.finishedWork = null;
1835
- root.remainingExpirationTime = commitRoot(finishedWork);
1839
+ commitRoot(root, finishedWork);
1840
}
1841
1842
// When working on async work, the reconciler asks the renderer if it should
1843
// yield execution. For DOM, we implement this with requestIdleCallback.
1844
function shouldYield() {
1841
- if (deadline === null) {
1845
+ if (deadline === null || deadlineDidExpire) {
1846
return false;
1847
}
1848
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
1862
);
1863
// Unschedule this root so we don't work on it again until there's
1864
// another update.
1861
- nextFlushedRoot.remainingExpirationTime = NoWork;
1865
+ nextFlushedRoot.expirationTime = NoWork;
1866
if (!hasUnhandledError) {
1867
hasUnhandledError = true;
1868
unhandledError = error;
1869
}
1870
}
1871
1868
-function onBlock(remainingExpirationTime: ExpirationTime) {
1869
- invariant(
1870
- nextFlushedRoot !== null,
1871
- 'Should be working on a root. This error is likely caused by a bug in ' +
1872
- 'React. Please file an issue.',
1873
- );
1874
- // This root was blocked. Unschedule it until there's another update.
1875
- nextFlushedRoot.remainingExpirationTime = remainingExpirationTime;
1876
-}
1877
-
1872
// TODO: Batching should be implemented at the renderer level, not inside
1873
// the reconciler.
1874
function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
1930
lowestPendingInteractiveExpirationTime !== NoWork
1931
) {
1932
// Synchronously flush pending interactive updates.
1939
- performWork(lowestPendingInteractiveExpirationTime, false, null);
1933
+ performWork(lowestPendingInteractiveExpirationTime, null);
1934
lowestPendingInteractiveExpirationTime = NoWork;
1935
}
1936
const previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
1951
function flushInteractiveUpdates() {
1952
if (!isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
1953
// Synchronously flush pending interactive updates.
1960
- performWork(lowestPendingInteractiveExpirationTime, false, null);
1954
+ performWork(lowestPendingInteractiveExpirationTime, null);
1955
lowestPendingInteractiveExpirationTime = NoWork;
1956
}
1957
}
1964
} finally {
1965
isBatchingUpdates = previousIsBatchingUpdates;
1966
if (!isBatchingUpdates && !isRendering) {
1973
- performWork(Sync, false, null);
1967
+ performWork(Sync, null);
1968
}
1969
}
1970
}