Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/react-reconciler/src/ReactFiberExpirationTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ export type ExpirationTime = number;

const NoWork = 0;
const Sync = 1;
const Task = 2;
const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 3;
const MAGIC_NUMBER_OFFSET = 2;

exports.Sync = Sync;
exports.Task = Task;
exports.NoWork = NoWork;
exports.Never = Never;

Expand Down
242 changes: 136 additions & 106 deletions packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ var {createWorkInProgress} = require('./ReactFiber');
var {onCommitRoot} = require('./ReactFiberDevToolsHook');
var {
NoWork,
Task,
Sync,
Never,
msToExpirationTime,
Expand Down Expand Up @@ -1172,15 +1171,6 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
expirationTime = computeAsyncExpiration();
}
}

if (
expirationTime === Sync &&
isBatchingUpdates &&
(!isUnbatchingUpdates || isCommitting)
) {
// If we're in a batch, downgrade sync to task.
expirationTime = Task;
}
return expirationTime;
}

Expand Down Expand Up @@ -1250,7 +1240,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
}

function scheduleErrorRecovery(fiber: Fiber) {
scheduleWorkImpl(fiber, Task, true);
scheduleWorkImpl(fiber, Sync, true);
}

function recalculateCurrentTime(): ExpirationTime {
Expand Down Expand Up @@ -1318,15 +1308,18 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
);
}

// Add the root to the schedule.
// Check if this root is already part of the schedule.
if (root.remainingExpirationTime === NoWork) {
if (root.nextScheduledRoot === null) {
// This root is not already scheduled. Add it.
root.remainingExpirationTime = expirationTime;
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
} else {
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
}
} else {
// This root is already scheduled, but its priority may have increased.
Expand All @@ -1340,56 +1333,89 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
}
}

// If we're not already rendering, schedule work to flush now (if it's
// sync) or later (if it's async).
if (!isRendering) {
// TODO: Remove distinction between sync and task. Maybe we can remove
// these magic numbers entirely by always comparing to the current time?
if (expirationTime === Sync) {
if (isUnbatchingUpdates) {
performWork(Sync, null);
} else {
performWork(Task, null);
}
} else if (!isCallbackScheduled) {
isCallbackScheduled = true;
scheduleDeferredCallback(flushAsyncWork);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}

if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
performWorkOnRoot(root, Sync);
}
return;
}

// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performWork(Sync, null);
} else if (!isCallbackScheduled) {
isCallbackScheduled = true;
scheduleDeferredCallback(performAsyncWork);
}
}

function findHighestPriorityRoot() {
let highestPriorityWork = NoWork;
let highestPriorityRoot = null;

let previousScheduledRoot = null;
let root = firstScheduledRoot;
while (root !== null) {
const remainingExpirationTime = root.remainingExpirationTime;
if (remainingExpirationTime === NoWork) {
// If this root no longer has work, remove it from the scheduler.
let next = root.nextScheduledRoot;
root.nextScheduledRoot = null;
if (previousScheduledRoot === null) {
firstScheduledRoot = next;
if (lastScheduledRoot !== null) {
let previousScheduledRoot = lastScheduledRoot;
let root = firstScheduledRoot;
while (root !== null) {
const remainingExpirationTime = root.remainingExpirationTime;
if (remainingExpirationTime === NoWork) {
// This root no longer has work. Remove it from the scheduler.

// TODO: This check is redudant, but Flow is confused by the branch
// below where we set lastScheduledRoot to null, even though we break
// from the loop right after.
invariant(
previousScheduledRoot !== null && lastScheduledRoot !== null,
'Should have a previous and last root. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
if (root === root.nextScheduledRoot) {
// This is the only root in the list.
root.nextScheduledRoot = null;
firstScheduledRoot = lastScheduledRoot = null;
break;
} else if (root === firstScheduledRoot) {
// This is the first root in the list.
const next = root.nextScheduledRoot;
firstScheduledRoot = next;
lastScheduledRoot.nextScheduledRoot = next;
root.nextScheduledRoot = null;
} else if (root === lastScheduledRoot) {
// This is the last root in the list.
lastScheduledRoot = previousScheduledRoot;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
root.nextScheduledRoot = null;
break;
} else {
previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot;
root.nextScheduledRoot = null;
}
root = previousScheduledRoot.nextScheduledRoot;
} else {
previousScheduledRoot.nextScheduledRoot = next;
}
if (next === null) {
lastScheduledRoot = null;
if (
highestPriorityWork === NoWork ||
remainingExpirationTime < highestPriorityWork
) {
// Update the priority, if it's higher
highestPriorityWork = remainingExpirationTime;
highestPriorityRoot = root;
}
if (root === lastScheduledRoot) {
break;
}
previousScheduledRoot = root;
root = root.nextScheduledRoot;
}
root = next;
continue;
} else if (
highestPriorityWork === NoWork ||
remainingExpirationTime < highestPriorityWork
) {
// Update the priority, if it's higher
highestPriorityWork = remainingExpirationTime;
highestPriorityRoot = root;
}
previousScheduledRoot = root;
root = root.nextScheduledRoot;
}

// If the next root is the same as the previous root, this is a nested
Expand All @@ -1408,18 +1434,11 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
nextFlushedExpirationTime = highestPriorityWork;
}

function flushAsyncWork(dl) {
function performAsyncWork(dl) {
performWork(NoWork, dl);
}

function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
invariant(
!isRendering,
'performWork was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);

isRendering = true;
deadline = dl;

// Keep working on roots until there's no more work, or until the we reach
Expand All @@ -1432,49 +1451,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
nextFlushedExpirationTime <= minExpirationTime) &&
!deadlineDidExpire
) {
// Check if this is async work or sync/expired work.
// TODO: Pass current time as argument to renderRoot, commitRoot
if (nextFlushedExpirationTime <= recalculateCurrentTime()) {
// Flush sync work.
let finishedWork = nextFlushedRoot.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
nextFlushedRoot.finishedWork = null;
nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
} else {
nextFlushedRoot.finishedWork = null;
finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
if (finishedWork !== null) {
// We've completed the root. Commit it.
nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
}
}
} else {
// Flush async work.
let finishedWork = nextFlushedRoot.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
nextFlushedRoot.finishedWork = null;
nextFlushedRoot.remainingExpirationTime = commitRoot(finishedWork);
} else {
nextFlushedRoot.finishedWork = null;
finishedWork = renderRoot(nextFlushedRoot, nextFlushedExpirationTime);
if (finishedWork !== null) {
// We've completed the root. Check the deadline one more time
// before committing.
if (!shouldYield()) {
// Still time left. Commit the root.
nextFlushedRoot.remainingExpirationTime = commitRoot(
finishedWork,
);
} else {
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
nextFlushedRoot.finishedWork = finishedWork;
}
}
}
}
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
// Find the next highest priority work.
findHighestPriorityRoot();
}
Expand All @@ -1489,13 +1466,12 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
// If there's work left over, schedule a new callback.
if (nextFlushedRoot !== null && !isCallbackScheduled) {
isCallbackScheduled = true;
scheduleDeferredCallback(flushAsyncWork);
scheduleDeferredCallback(performAsyncWork);
}

// Clean-up.
deadline = null;
deadlineDidExpire = false;
isRendering = false;
nestedUpdateCount = 0;

if (hasUnhandledError) {
Expand All @@ -1506,6 +1482,60 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
}
}

function performWorkOnRoot(root, expirationTime) {
invariant(
!isRendering,
'performWorkOnRoot was called recursively. This error is likely caused ' +
'by a bug in React. Please file an issue.',
);

isRendering = true;

// Check if this is async work or sync/expired work.
// TODO: Pass current time as argument to renderRoot, commitRoot
if (expirationTime <= recalculateCurrentTime()) {
// Flush sync work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
root.finishedWork = null;
root.remainingExpirationTime = commitRoot(finishedWork);
} else {
root.finishedWork = null;
finishedWork = renderRoot(root, expirationTime);
if (finishedWork !== null) {
// We've completed the root. Commit it.
root.remainingExpirationTime = commitRoot(finishedWork);
}
}
} else {
// Flush async work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
root.finishedWork = null;
root.remainingExpirationTime = commitRoot(finishedWork);
} else {
root.finishedWork = null;
finishedWork = renderRoot(root, expirationTime);
if (finishedWork !== null) {
// We've completed the root. Check the deadline one more time
// before committing.
if (!shouldYield()) {
// Still time left. Commit the root.
root.remainingExpirationTime = commitRoot(finishedWork);
} else {
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
root.finishedWork = finishedWork;
}
}
}
}

isRendering = false;
}

// When working on async work, the reconciler asks the renderer if it should
// yield execution. For DOM, we implement this with requestIdleCallback.
function shouldYield() {
Expand Down Expand Up @@ -1546,7 +1576,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
performWork(Task, null);
performWork(Sync, null);
}
}
}
Expand Down Expand Up @@ -1579,7 +1609,7 @@ module.exports = function<T, P, I, TI, PI, C, CC, CX, PL>(
'flushSync was called from inside a lifecycle method. It cannot be ' +
'called when React is already rendering.',
);
performWork(Task, null);
performWork(Sync, null);
}
}

Expand Down