Initial Lanes implementation (#18796)

See PR #18796 for more information.

All of the changes I've made in this commit are behind the
`enableNewReconciler` flag. Merging this to master will not affect the
open source builds or the build that we ship to Facebook.

The only build that is affected is the `ReactDOMForked` build, which is
deployed to Facebook **behind an experimental flag (currently disabled
for all users)**. We will use this flag to gradually roll out the new
reconciler, and quickly roll it back if we find any problems.

Because we have those protections in place, what I'm aiming for with
this initial PR is the **smallest possible atomic change that lands
cleanly and doesn't rely on too many hacks**. The goal has not been to
get every single test or feature passing, and it definitely is not to
implement all the features that we intend to build on top of the new
model. When possible, I have chosen to preserve existing semantics and
defer changes to follow-up steps. (Listed in the section below.)

(I did not end up having to disable any tests, although if I had, that
should not have necessarily been a merge blocker.)

For example, even though one of the primary goals of this project is to
improve our model for parallel Suspense transitions, in this initial
implementation, I have chosen to keep the same core heuristics for
sequencing and flushing that existed in the ExpirationTimes model: low
priority updates cannot finish without also finishing high priority
ones.

Despite all these precautions, **because the scope of this refactor is
inherently large, I do expect we will find regressions.** The flip side
is that I also expect the new model to improve the stability of the
codebase and make it easier to fix bugs when they arise.
This commit is contained in:
Andrew Clark
2020-05-02 17:09:31 -07:00
committed by GitHub
parent 3c7d52c3d6
commit 93e078ddf2
28 changed files with 1948 additions and 2263 deletions
@@ -1306,44 +1306,25 @@ describe('ReactDOMServerHooks', () => {
// State update should trigger the ID to update, which changes the props
// of ChildWithID. This should cause ChildWithID to hydrate before Children
gate(flags => {
if (__DEV__) {
expect(Scheduler).toFlushAndYieldThrough([
'Child with ID',
// Fallbacks are immdiately committed in TestUtils version
// of act
// 'Child with ID',
// 'Child with ID',
'Child One',
'Child Two',
]);
} else if (flags.new) {
// Upgrading a dehyrdating boundary works a little differently in
// the new reconciler. After the update on the boundary is
// scheduled, it waits until the end of the current time slice
// before restarting at the higher priority.
expect(Scheduler).toFlushAndYieldThrough([
'Child with ID',
'Child with ID',
'Child with ID',
'Child with ID',
'Child One',
'Child Two',
]);
} else {
// Whereas the old reconciler relies on a Scheduler hack to
// interrupt the current task. It's not clear if this is any
// better or worse, though. Regardless it's not a big deal since
// the time slices aren't that big.
expect(Scheduler).toFlushAndYieldThrough([
'Child with ID',
'Child with ID',
'Child with ID',
'Child One',
'Child Two',
]);
}
});
expect(Scheduler).toFlushAndYieldThrough(
__DEV__
? [
'Child with ID',
// Fallbacks are immediately committed in TestUtils version
// of act
// 'Child with ID',
// 'Child with ID',
'Child One',
'Child Two',
]
: [
'Child with ID',
'Child with ID',
'Child with ID',
'Child One',
'Child Two',
],
);
expect(child1Ref.current).toBe(null);
expect(childWithIDRef.current).toEqual(
@@ -1691,15 +1672,24 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
if (gate(flags => flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
]);
} else {
// In the old reconciler, the error isn't surfaced to the user. That
// part isn't important, as long as It warns.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
}
});
it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
@@ -1724,15 +1714,24 @@ describe('ReactDOMServerHooks', () => {
ReactDOM.createRoot(container, {hydrate: true}).render(<App />);
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
if (gate(flags => flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
]);
} else {
// In the old reconciler, the error isn't surfaced to the user. That
// part isn't important, as long as It warns.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
),
).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
}
});
it('useOpaqueIdentifier with two opaque identifiers on the same page', () => {
@@ -12,7 +12,7 @@ import type {ReactPortal} from 'shared/ReactTypes';
import type {BlockComponent} from 'react/src/ReactBlock';
import type {LazyComponent} from 'react/src/ReactLazy';
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import getComponentName from 'shared/getComponentName';
import {Placement, Deletion} from './ReactSideEffectTags';
@@ -377,15 +377,11 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
textContent: string,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
) {
if (current === null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
@@ -400,7 +396,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
if (current !== null) {
if (
@@ -442,11 +438,7 @@ function ChildReconciler(shouldTrackSideEffects) {
}
}
// Insert
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
@@ -456,7 +448,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
if (
current === null ||
@@ -465,11 +457,7 @@ function ChildReconciler(shouldTrackSideEffects) {
current.stateNode.implementation !== portal.implementation
) {
// Insert
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
@@ -484,7 +472,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
current: Fiber | null,
fragment: Iterable<*>,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
): Fiber {
if (current === null || current.tag !== Fragment) {
@@ -492,7 +480,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromFragment(
fragment,
returnFiber.mode,
expirationTime,
lanes,
key,
);
created.return = returnFiber;
@@ -508,7 +496,7 @@ function ChildReconciler(shouldTrackSideEffects) {
function createChild(
returnFiber: Fiber,
newChild: any,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes don't have keys. If the previous node is implicitly keyed
@@ -517,7 +505,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromText(
'' + newChild,
returnFiber.mode,
expirationTime,
lanes,
);
created.return = returnFiber;
return created;
@@ -529,7 +517,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromElement(
newChild,
returnFiber.mode,
expirationTime,
lanes,
);
created.ref = coerceRef(returnFiber, null, newChild);
created.return = returnFiber;
@@ -539,7 +527,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromPortal(
newChild,
returnFiber.mode,
expirationTime,
lanes,
);
created.return = returnFiber;
return created;
@@ -550,7 +538,7 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
expirationTime,
lanes,
null,
);
created.return = returnFiber;
@@ -573,7 +561,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
// Update the fiber if the keys match, otherwise return null.
@@ -586,12 +574,7 @@ function ChildReconciler(shouldTrackSideEffects) {
if (key !== null) {
return null;
}
return updateTextNode(
returnFiber,
oldFiber,
'' + newChild,
expirationTime,
);
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
@@ -603,28 +586,18 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
oldFiber,
newChild.props.children,
expirationTime,
lanes,
key,
);
}
return updateElement(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(
returnFiber,
oldFiber,
newChild,
expirationTime,
);
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
@@ -636,13 +609,7 @@ function ChildReconciler(shouldTrackSideEffects) {
return null;
}
return updateFragment(
returnFiber,
oldFiber,
newChild,
expirationTime,
null,
);
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -662,18 +629,13 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
newIdx: number,
newChild: any,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes don't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
const matchedFiber = existingChildren.get(newIdx) || null;
return updateTextNode(
returnFiber,
matchedFiber,
'' + newChild,
expirationTime,
);
return updateTextNode(returnFiber, matchedFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
@@ -688,40 +650,24 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
matchedFiber,
newChild.props.children,
expirationTime,
lanes,
newChild.key,
);
}
return updateElement(
returnFiber,
matchedFiber,
newChild,
expirationTime,
);
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
const matchedFiber =
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
return updatePortal(
returnFiber,
matchedFiber,
newChild,
expirationTime,
);
return updatePortal(returnFiber, matchedFiber, newChild, lanes);
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
const matchedFiber = existingChildren.get(newIdx) || null;
return updateFragment(
returnFiber,
matchedFiber,
newChild,
expirationTime,
null,
);
return updateFragment(returnFiber, matchedFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
@@ -785,7 +731,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
// This algorithm can't optimize by searching from both ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
@@ -833,7 +779,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime,
lanes,
);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
@@ -877,11 +823,7 @@ function ChildReconciler(shouldTrackSideEffects) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
@@ -907,7 +849,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime,
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
@@ -944,7 +886,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildrenIterable: Iterable<*>,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
// This is the same implementation as reconcileChildrenArray(),
// but using the iterator instead.
@@ -1023,12 +965,7 @@ function ChildReconciler(shouldTrackSideEffects) {
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
step.value,
expirationTime,
);
const newFiber = updateSlot(returnFiber, oldFiber, step.value, lanes);
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
@@ -1071,7 +1008,7 @@ function ChildReconciler(shouldTrackSideEffects) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, expirationTime);
const newFiber = createChild(returnFiber, step.value, lanes);
if (newFiber === null) {
continue;
}
@@ -1097,7 +1034,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
newIdx,
step.value,
expirationTime,
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
@@ -1134,7 +1071,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
@@ -1149,11 +1086,7 @@ function ChildReconciler(shouldTrackSideEffects) {
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(
textContent,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
@@ -1162,7 +1095,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
@@ -1245,17 +1178,13 @@ function ChildReconciler(shouldTrackSideEffects) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
@@ -1266,7 +1195,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
portal: ReactPortal,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
const key = portal.key;
let child = currentFirstChild;
@@ -1293,11 +1222,7 @@ function ChildReconciler(shouldTrackSideEffects) {
child = child.sibling;
}
const created = createFiberFromPortal(
portal,
returnFiber.mode,
expirationTime,
);
const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
@@ -1309,7 +1234,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber | null {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
@@ -1339,7 +1264,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
expirationTime,
lanes,
),
);
case REACT_PORTAL_TYPE:
@@ -1348,7 +1273,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
expirationTime,
lanes,
),
);
}
@@ -1360,7 +1285,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
'' + newChild,
expirationTime,
lanes,
),
);
}
@@ -1370,7 +1295,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
expirationTime,
lanes,
);
}
@@ -1379,7 +1304,7 @@ function ChildReconciler(shouldTrackSideEffects) {
returnFiber,
currentFirstChild,
newChild,
expirationTime,
lanes,
);
}
@@ -1462,13 +1387,10 @@ export function cloneChildFibers(
}
// Reset a workInProgress child set to prepare it for a second pass.
export function resetChildFibers(
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
): void {
export function resetChildFibers(workInProgress: Fiber, lanes: Lanes): void {
let child = workInProgress.child;
while (child !== null) {
resetWorkInProgress(child, renderExpirationTime);
resetWorkInProgress(child, lanes);
child = child.sibling;
}
}
+48 -72
View File
@@ -18,7 +18,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {RootTag} from './ReactRootTags';
import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {OffscreenProps} from './ReactFiberOffscreenComponent';
@@ -66,7 +66,7 @@ import {
resolveFunctionForHotReloading,
resolveForwardRefForHotReloading,
} from './ReactFiberHotReloading.new';
import {NoWork} from './ReactFiberExpirationTime.new';
import {NoLanes} from './ReactFiberLane';
import {
NoMode,
ConcurrentMode,
@@ -150,8 +150,8 @@ function FiberNode(
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime_opaque = NoWork;
this.childExpirationTime_opaque = NoWork;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
@@ -314,9 +314,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
}
}
workInProgress.childExpirationTime_opaque =
current.childExpirationTime_opaque;
workInProgress.expirationTime_opaque = current.expirationTime_opaque;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
@@ -330,7 +329,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
@@ -368,10 +367,7 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
}
// Used to reuse a Fiber for a second pass.
export function resetWorkInProgress(
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
) {
export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
// This resets the Fiber to what createFiber or createWorkInProgress would
// have set the values to before during the first pass. Ideally this wouldn't
// be necessary but unfortunately many code paths reads from the workInProgress
@@ -392,8 +388,8 @@ export function resetWorkInProgress(
const current = workInProgress.alternate;
if (current === null) {
// Reset to createFiber's initial values.
workInProgress.childExpirationTime_opaque = NoWork;
workInProgress.expirationTime_opaque = renderExpirationTime;
workInProgress.childLanes = NoLanes;
workInProgress.lanes = renderLanes;
workInProgress.child = null;
workInProgress.memoizedProps = null;
@@ -412,9 +408,8 @@ export function resetWorkInProgress(
}
} else {
// Reset to the cloned values that createWorkInProgress would've.
workInProgress.childExpirationTime_opaque =
current.childExpirationTime_opaque;
workInProgress.expirationTime_opaque = current.expirationTime_opaque;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
@@ -428,7 +423,7 @@ export function resetWorkInProgress(
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
@@ -470,7 +465,7 @@ export function createFiberFromTypeAndProps(
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
let fiberTag = IndeterminateComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
@@ -491,12 +486,7 @@ export function createFiberFromTypeAndProps(
} else {
getTag: switch (type) {
case REACT_FRAGMENT_TYPE:
return createFiberFromFragment(
pendingProps.children,
mode,
expirationTime,
key,
);
return createFiberFromFragment(pendingProps.children, mode, lanes, key);
case REACT_DEBUG_TRACING_MODE_TYPE:
fiberTag = Mode;
mode |= DebugTracingMode;
@@ -506,30 +496,16 @@ export function createFiberFromTypeAndProps(
mode |= StrictMode;
break;
case REACT_PROFILER_TYPE:
return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
return createFiberFromProfiler(pendingProps, mode, lanes, key);
case REACT_SUSPENSE_TYPE:
return createFiberFromSuspense(pendingProps, mode, expirationTime, key);
return createFiberFromSuspense(pendingProps, mode, lanes, key);
case REACT_SUSPENSE_LIST_TYPE:
return createFiberFromSuspenseList(
pendingProps,
mode,
expirationTime,
key,
);
return createFiberFromSuspenseList(pendingProps, mode, lanes, key);
case REACT_OFFSCREEN_TYPE:
return createFiberFromOffscreen(
pendingProps,
mode,
expirationTime,
key,
);
return createFiberFromOffscreen(pendingProps, mode, lanes, key);
case REACT_LEGACY_HIDDEN_TYPE:
return createFiberFromLegacyHidden(
pendingProps,
mode,
expirationTime,
key,
);
return createFiberFromLegacyHidden(pendingProps, mode, lanes, key);
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
@@ -562,7 +538,7 @@ export function createFiberFromTypeAndProps(
type,
pendingProps,
mode,
expirationTime,
lanes,
key,
);
}
@@ -573,7 +549,7 @@ export function createFiberFromTypeAndProps(
type,
pendingProps,
mode,
expirationTime,
lanes,
key,
);
}
@@ -612,7 +588,7 @@ export function createFiberFromTypeAndProps(
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
@@ -620,7 +596,7 @@ export function createFiberFromTypeAndProps(
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
let owner = null;
if (__DEV__) {
@@ -635,7 +611,7 @@ export function createFiberFromElement(
pendingProps,
owner,
mode,
expirationTime,
lanes,
);
if (__DEV__) {
fiber._debugSource = element._source;
@@ -647,11 +623,11 @@ export function createFiberFromElement(
export function createFiberFromFragment(
elements: ReactFragment,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
): Fiber {
const fiber = createFiber(Fragment, elements, key, mode);
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
@@ -659,13 +635,13 @@ export function createFiberFromFundamental(
fundamentalComponent: ReactFundamentalComponent<any, any>,
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
): Fiber {
const fiber = createFiber(FundamentalComponent, pendingProps, key, mode);
fiber.elementType = fundamentalComponent;
fiber.type = fundamentalComponent;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
@@ -673,20 +649,20 @@ function createFiberFromScope(
scope: ReactScope,
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(ScopeComponent, pendingProps, key, mode);
fiber.type = scope;
fiber.elementType = scope;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
function createFiberFromProfiler(
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
): Fiber {
if (__DEV__) {
@@ -699,7 +675,7 @@ function createFiberFromProfiler(
// TODO: The Profiler fiber shouldn't have a type. It has a tag.
fiber.elementType = REACT_PROFILER_TYPE;
fiber.type = REACT_PROFILER_TYPE;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
if (enableProfilerTimer) {
fiber.stateNode = {
@@ -714,7 +690,7 @@ function createFiberFromProfiler(
export function createFiberFromSuspense(
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(SuspenseComponent, pendingProps, key, mode);
@@ -725,14 +701,14 @@ export function createFiberFromSuspense(
fiber.type = REACT_SUSPENSE_TYPE;
fiber.elementType = REACT_SUSPENSE_TYPE;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromSuspenseList(
pendingProps: any,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(SuspenseListComponent, pendingProps, key, mode);
@@ -743,14 +719,14 @@ export function createFiberFromSuspenseList(
fiber.type = REACT_SUSPENSE_LIST_TYPE;
}
fiber.elementType = REACT_SUSPENSE_LIST_TYPE;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromOffscreen(
pendingProps: OffscreenProps,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(OffscreenComponent, pendingProps, key, mode);
@@ -761,14 +737,14 @@ export function createFiberFromOffscreen(
fiber.type = REACT_OFFSCREEN_TYPE;
}
fiber.elementType = REACT_OFFSCREEN_TYPE;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromLegacyHidden(
pendingProps: OffscreenProps,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
key: null | string,
) {
const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode);
@@ -779,17 +755,17 @@ export function createFiberFromLegacyHidden(
fiber.type = REACT_LEGACY_HIDDEN_TYPE;
}
fiber.elementType = REACT_LEGACY_HIDDEN_TYPE;
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
export function createFiberFromText(
content: string,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
const fiber = createFiber(HostText, content, null, mode);
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
return fiber;
}
@@ -812,11 +788,11 @@ export function createFiberFromDehydratedFragment(
export function createFiberFromPortal(
portal: ReactPortal,
mode: TypeOfMode,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
): Fiber {
const pendingProps = portal.children !== null ? portal.children : [];
const fiber = createFiber(HostPortal, pendingProps, portal.key, mode);
fiber.expirationTime_opaque = expirationTime;
fiber.lanes = lanes;
fiber.stateNode = {
containerInfo: portal.containerInfo,
pendingChildren: null, // Used by persistent updates
@@ -862,8 +838,8 @@ export function assignFiberPropertiesInDEV(
target.nextEffect = source.nextEffect;
target.firstEffect = source.firstEffect;
target.lastEffect = source.lastEffect;
target.expirationTime_opaque = source.expirationTime_opaque;
target.childExpirationTime_opaque = source.childExpirationTime_opaque;
target.lanes = source.lanes;
target.childLanes = source.childLanes;
target.alternate = source.alternate;
if (enableProfilerTimer) {
target.actualDuration = source.actualDuration;
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import type {UpdateQueue} from './ReactUpdateQueue.new';
import * as React from 'react';
@@ -40,7 +40,7 @@ import {
initializeUpdateQueue,
cloneUpdateQueue,
} from './ReactUpdateQueue.new';
import {NoWork, isSameExpirationTime} from './ReactFiberExpirationTime.new';
import {NoLanes} from './ReactFiberLane';
import {
cacheContext,
getMaskedContext,
@@ -51,7 +51,7 @@ import {
import {readContext} from './ReactFiberNewContext.new';
import {
requestEventTime,
requestUpdateExpirationTime,
requestUpdateLane,
scheduleUpdateOnFiber,
} from './ReactFiberWorkLoop.new';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
@@ -177,12 +177,7 @@ export function applyDerivedStateFromProps(
// Once the update queue is empty, persist the derived state onto the
// base state.
if (
isSameExpirationTime(
workInProgress.expirationTime_opaque,
(NoWork: ExpirationTimeOpaque),
)
) {
if (workInProgress.lanes === NoLanes) {
// Queue is always non-null for classes
const updateQueue: UpdateQueue<any> = (workInProgress.updateQueue: any);
updateQueue.baseState = memoizedState;
@@ -195,9 +190,9 @@ const classComponentUpdater = {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
@@ -207,15 +202,15 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, lane);
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
update.tag = ReplaceState;
update.payload = payload;
@@ -227,15 +222,15 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, lane);
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
const lane = requestUpdateLane(fiber, suspenseConfig);
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
update.tag = ForceUpdate;
if (callback !== undefined && callback !== null) {
@@ -246,7 +241,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleUpdateOnFiber(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, lane);
},
};
@@ -771,7 +766,7 @@ function mountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): void {
if (__DEV__) {
checkClassInstance(workInProgress, ctor, newProps);
@@ -823,7 +818,7 @@ function mountClassInstance(
}
}
processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
@@ -848,12 +843,7 @@ function mountClassInstance(
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
processUpdateQueue(
workInProgress,
newProps,
instance,
renderExpirationTime,
);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
instance.state = workInProgress.memoizedState;
}
@@ -866,7 +856,7 @@ function resumeMountClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): boolean {
const instance = workInProgress.stateNode;
@@ -917,7 +907,7 @@ function resumeMountClassInstance(
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;
if (
oldProps === newProps &&
@@ -1001,7 +991,7 @@ function updateClassInstance(
workInProgress: Fiber,
ctor: any,
newProps: any,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): boolean {
const instance = workInProgress.stateNode;
@@ -1058,7 +1048,7 @@ function updateClassInstance(
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
processUpdateQueue(workInProgress, newProps, instance, renderExpirationTime);
processUpdateQueue(workInProgress, newProps, instance, renderLanes);
newState = workInProgress.memoizedState;
if (
@@ -17,7 +17,7 @@ import type {
} from './ReactFiberHostConfig';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import type {UpdateQueue} from './ReactUpdateQueue.new';
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
@@ -516,7 +516,7 @@ function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedExpirationTime: ExpirationTimeOpaque,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
@@ -8,7 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import type {
ReactFundamentalComponentInstance,
ReactScopeInstance,
@@ -133,10 +133,10 @@ import {
renderDidSuspend,
renderDidSuspendDelayIfPossible,
renderHasNotSuspendedYet,
popRenderExpirationTime,
popRenderLanes,
} from './ReactFiberWorkLoop.new';
import {createFundamentalStateInstance} from './ReactFiberFundamental.new';
import {Never, isSameOrHigherPriority} from './ReactFiberExpirationTime.new';
import {OffscreenLane} from './ReactFiberLane';
import {resetChildFibers} from './ReactChildFiber.new';
import {updateDeprecatedEventListeners} from './ReactFiberDeprecatedEvents.new';
import {createScopeInstance} from './ReactFiberScope.new';
@@ -644,7 +644,7 @@ function cutOffTailIfNeeded(
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
@@ -857,7 +857,7 @@ function completeWork(
);
prepareToHydrateHostSuspenseInstance(workInProgress);
if (enableSchedulerTracing) {
markSpawnedWork((Never: ExpirationTimeOpaque));
markSpawnedWork(OffscreenLane);
}
return null;
} else {
@@ -882,7 +882,7 @@ function completeWork(
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
// Something suspended. Re-render with the fallback children.
workInProgress.expirationTime_opaque = renderExpirationTime;
workInProgress.lanes = renderLanes;
// Do not reset the effect list.
return workInProgress;
}
@@ -1051,7 +1051,7 @@ function completeWork(
}
workInProgress.lastEffect = renderState.lastEffect;
// Reset the child fibers to their original state.
resetChildFibers(workInProgress, renderExpirationTime);
resetChildFibers(workInProgress, renderLanes);
// Set up the Suspense Context to force suspense and immediately
// rerender the children.
@@ -1111,10 +1111,7 @@ function completeWork(
// the expiration.
now() * 2 - renderState.renderingStartTime >
renderState.tailExpiration &&
!isSameOrHigherPriority(
(Never: ExpirationTimeOpaque),
renderExpirationTime,
)
renderLanes !== OffscreenLane
) {
// We have now passed our CPU deadline and we'll just give up further
// attempts to render the main content and only render fallbacks.
@@ -1129,9 +1126,9 @@ function completeWork(
// them, then they really have the same priority as this render.
// So we'll pick it back up the very next render pass once we've had
// an opportunity to yield for paint.
workInProgress.expirationTime_opaque = renderExpirationTime;
workInProgress.lanes = renderLanes;
if (enableSchedulerTracing) {
markSpawnedWork(renderExpirationTime);
markSpawnedWork(renderLanes);
}
}
}
@@ -1297,7 +1294,7 @@ function completeWork(
break;
case OffscreenComponent:
case LegacyHiddenComponent: {
popRenderExpirationTime(workInProgress);
popRenderLanes(workInProgress);
if (current !== null) {
const nextState: OffscreenState | null = workInProgress.memoizedState;
const prevState: OffscreenState | null = current.memoizedState;
@@ -19,7 +19,7 @@ import {
DEPRECATED_mountResponderInstance,
DEPRECATED_unmountResponderInstance,
} from './ReactFiberHostConfig';
import {NoWork} from './ReactFiberExpirationTime.new';
import {NoLanes} from './ReactFiberLane';
import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols';
@@ -154,7 +154,7 @@ export function updateDeprecatedEventListeners(
if (listeners != null) {
if (dependencies === null) {
dependencies = fiber.dependencies_new = {
expirationTime: NoWork,
lanes: NoLanes,
firstContext: null,
responders: new Map(),
};
@@ -1,92 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactPriorityLevel} from './ReactInternalTypes';
import {MAX_SIGNED_31_BIT_INT} from './MaxInts';
import {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
IdlePriority,
} from './SchedulerWithReactIntegration.new';
export opaque type ExpirationTimeOpaque = number;
export const NoWork: ExpirationTimeOpaque = 0;
// TODO: Think of a better name for Never. The key difference with Idle is that
// Never work can be committed in an inconsistent state without tearing the UI.
// The main example is offscreen content, like a hidden subtree. So one possible
// name is Offscreen. However, it also includes dehydrated Suspense boundaries,
// which are inconsistent in the sense that they haven't finished yet, but
// aren't visibly inconsistent because the server rendered HTML matches what the
// hydrated tree would look like.
export const Never: ExpirationTimeOpaque = 1;
// Idle is slightly higher priority than Never. It must completely finish in
// order to be consistent.
export const Idle: ExpirationTimeOpaque = 2;
// Continuous Hydration is slightly higher than Idle and is used to increase
// priority of hover targets.
export const ContinuousHydration: ExpirationTimeOpaque = 3;
export const LongTransition: ExpirationTimeOpaque = 49999;
export const ShortTransition: ExpirationTimeOpaque = 99999;
export const DefaultUpdateTime: ExpirationTimeOpaque = 1073741296;
export const UserBlockingUpdateTime: ExpirationTimeOpaque = 1073741761;
export const Sync: ExpirationTimeOpaque = MAX_SIGNED_31_BIT_INT;
export const Batched: ExpirationTimeOpaque = Sync - 1;
// Accounts for -1 trick to bump updates into a different batch
const ADJUSTMENT_OFFSET = 5;
export function inferPriorityFromExpirationTime(
expirationTime: ExpirationTimeOpaque,
): ReactPriorityLevel {
if (expirationTime >= Batched - ADJUSTMENT_OFFSET) {
return ImmediatePriority;
}
if (expirationTime >= UserBlockingUpdateTime - ADJUSTMENT_OFFSET) {
return UserBlockingPriority;
}
if (expirationTime >= LongTransition - ADJUSTMENT_OFFSET) {
return NormalPriority;
}
// TODO: Handle LowPriority. Maybe should give it NormalPriority since Idle is
// very agressively deprioritized.
// Assume anything lower has idle priority
return IdlePriority;
}
export function isSameOrHigherPriority(
a: ExpirationTimeOpaque,
b: ExpirationTimeOpaque,
) {
return a >= b;
}
export function isSameExpirationTime(
a: ExpirationTimeOpaque,
b: ExpirationTimeOpaque,
) {
return a === b;
}
export function bumpPriorityHigher(
a: ExpirationTimeOpaque,
): ExpirationTimeOpaque {
return a + 1;
}
export function bumpPriorityLower(
a: ExpirationTimeOpaque,
): ExpirationTimeOpaque {
return a - 1;
}
@@ -16,7 +16,7 @@ import type {
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {Fiber, Dispatcher} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes, Lane} from './ReactFiberLane';
import type {HookEffectTag} from './ReactHookEffectTags';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import type {ReactPriorityLevel} from './ReactInternalTypes';
@@ -25,14 +25,16 @@ import type {OpaqueIDType} from './ReactFiberHostConfig';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {markRootExpiredAtTime} from './ReactFiberRoot.new';
import {
NoWork,
Sync,
isSameOrHigherPriority,
isSameExpirationTime,
} from './ReactFiberExpirationTime.new';
import {NoMode, BlockingMode} from './ReactTypeOfMode';
import {
NoLane,
NoLanes,
isSubsetOfLanes,
mergeLanes,
removeLanes,
markRootExpired,
markRootMutableRead,
} from './ReactFiberLane';
import {readContext} from './ReactFiberNewContext.new';
import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.new';
import {
@@ -47,13 +49,13 @@ import {
import {
getWorkInProgressRoot,
scheduleUpdateOnFiber,
requestUpdateExpirationTime,
requestUpdateLane,
requestEventTime,
warnIfNotCurrentlyActingEffectsInDEV,
warnIfNotCurrentlyActingUpdatesInDev,
warnIfNotScopedWithMatchingAct,
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
markSkippedUpdateLanes,
} from './ReactFiberWorkLoop.new';
import invariant from 'shared/invariant';
@@ -74,10 +76,8 @@ import {
makeOpaqueHydratingObject,
} from './ReactFiberHostConfig';
import {
getLastPendingExpirationTime,
getWorkInProgressVersion,
markSourceAsDirty,
setPendingExpirationTime,
setWorkInProgressVersion,
warnAboutMultipleRenderersDEV,
} from './ReactMutableSource.new';
@@ -89,7 +89,7 @@ type Update<S, A> = {|
// TODO: Temporary field. Will remove this by storing a map of
// transition -> start time on the root.
eventTime: number,
expirationTime: ExpirationTimeOpaque,
lane: Lane,
suspenseConfig: null | SuspenseConfig,
action: A,
eagerReducer: ((S, A) => S) | null,
@@ -156,7 +156,7 @@ type BasicStateAction<S> = (S => S) | S;
type Dispatch<A> = A => void;
// These are set right before calling the component.
let renderExpirationTime: ExpirationTimeOpaque = NoWork;
let renderLanes: Lanes = NoLanes;
// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
@@ -347,9 +347,9 @@ export function renderWithHooks<Props, SecondArg>(
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderExpirationTime: ExpirationTimeOpaque,
nextRenderLanes: Lanes,
): any {
renderExpirationTime = nextRenderExpirationTime;
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
if (__DEV__) {
@@ -365,7 +365,7 @@ export function renderWithHooks<Props, SecondArg>(
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime_opaque = NoWork;
workInProgress.lanes = NoLanes;
// The following should have already been reset
// currentHook = null;
@@ -454,7 +454,7 @@ export function renderWithHooks<Props, SecondArg>(
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderExpirationTime = NoWork;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
@@ -480,13 +480,11 @@ export function renderWithHooks<Props, SecondArg>(
export function bailoutHooks(
current: Fiber,
workInProgress: Fiber,
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
) {
workInProgress.updateQueue = current.updateQueue;
workInProgress.effectTag &= ~(PassiveEffect | UpdateEffect);
if (isSameOrHigherPriority(expirationTime, current.expirationTime_opaque)) {
current.expirationTime_opaque = NoWork;
}
current.lanes = removeLanes(current.lanes, lanes);
}
export function resetHooksAfterThrow(): void {
@@ -514,7 +512,7 @@ export function resetHooksAfterThrow(): void {
didScheduleRenderPhaseUpdate = false;
}
renderExpirationTime = NoWork;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
@@ -708,15 +706,15 @@ function updateReducer<S, I, A>(
let update = first;
do {
const suspenseConfig = update.suspenseConfig;
const updateExpirationTime = update.expirationTime;
const updateLane = update.lane;
const updateEventTime = update.eventTime;
if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<S, A> = {
eventTime: updateEventTime,
expirationTime: updateExpirationTime,
lane: updateLane,
suspenseConfig: suspenseConfig,
action: update.action,
eagerReducer: update.eagerReducer,
@@ -730,22 +728,23 @@ function updateReducer<S, I, A>(
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
if (
!isSameOrHigherPriority(
currentlyRenderingFiber.expirationTime_opaque,
updateExpirationTime,
)
) {
currentlyRenderingFiber.expirationTime_opaque = updateExpirationTime;
markUnprocessedUpdateTime(updateExpirationTime);
}
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
eventTime: updateEventTime,
expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
suspenseConfig: update.suspenseConfig,
action: update.action,
eagerReducer: update.eagerReducer,
@@ -882,26 +881,28 @@ function readFromUnsubcribedMutableSource<Source, Snapshot>(
// we can use it alone to determine if we can safely read from the source.
const currentRenderVersion = getWorkInProgressVersion(source);
if (currentRenderVersion !== null) {
// It's safe to read if the store hasn't been mutated since the last time
// we read something.
isSafeToReadFromSource = currentRenderVersion === version;
} else {
// If there's no version, then we should fallback to checking the update time.
const pendingExpirationTime = getLastPendingExpirationTime(root);
if (
isSameExpirationTime(
pendingExpirationTime,
(NoWork: ExpirationTimeOpaque),
)
) {
isSafeToReadFromSource = true;
} else {
// If the source has pending updates, we can use the current render's expiration
// time to determine if it's safe to read again from the source.
isSafeToReadFromSource = isSameOrHigherPriority(
pendingExpirationTime,
renderExpirationTime,
);
}
// If there's no version, then this is the first time we've read from the
// source during the current render pass, so we need to do a bit more work.
// What we need to determine is if there are any hooks that already
// subscribed to the source, and if so, whether there are any pending
// mutations that haven't been synchronized yet.
//
// If there are no pending mutations, then `root.mutableReadLanes` will be
// empty, and we know we can safely read.
//
// If there *are* pending mutations, we may still be able to safely read
// if the currently rendering lanes are inclusive of the pending mutation
// lanes, since that guarantees that the value we're about to read from
// the source is consistent with the values that we read during the most
// recent mutation.
isSafeToReadFromSource = isSubsetOfLanes(
renderLanes,
root.mutableReadLanes,
);
if (isSafeToReadFromSource) {
// If it's safe to read from this source during the current render,
@@ -994,19 +995,16 @@ function useMutableSource<Source, Snapshot>(
setSnapshot(maybeNewSnapshot);
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(
fiber,
suspenseConfig,
);
setPendingExpirationTime(root, expirationTime);
const lane = requestUpdateLane(fiber, suspenseConfig);
markRootMutableRead(root, lane);
// If the source mutated between render and now,
// there may be state updates already scheduled from the old getSnapshot.
// Those updates should not commit without this value.
// There is no mechanism currently to associate these updates though,
// so for now we fall back to synchronously flushing all pending updates.
// TODO: Improve this later.
markRootExpiredAtTime(root, getLastPendingExpirationTime(root));
// TODO: This should entangle the lanes instead of expiring everything.
markRootExpired(root, root.mutableReadLanes);
}
}
}, [getSnapshot, source, subscribe]);
@@ -1022,12 +1020,9 @@ function useMutableSource<Source, Snapshot>(
// Record a pending mutable source update with the same expiration time.
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(
fiber,
suspenseConfig,
);
const lane = requestUpdateLane(fiber, suspenseConfig);
setPendingExpirationTime(root, expirationTime);
markRootMutableRead(root, lane);
} catch (error) {
// A selector might throw after a source mutation.
// e.g. it might try to read from a part of the store that no longer exists.
@@ -1646,11 +1641,11 @@ function dispatchAction<S, A>(
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(fiber, suspenseConfig);
const lane = requestUpdateLane(fiber, suspenseConfig);
const update: Update<S, A> = {
eventTime,
expirationTime,
lane,
suspenseConfig,
action,
eagerReducer: null,
@@ -1678,18 +1673,10 @@ function dispatchAction<S, A>(
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
update.expirationTime = renderExpirationTime;
} else {
if (
isSameExpirationTime(
fiber.expirationTime_opaque,
(NoWork: ExpirationTimeOpaque),
) &&
(alternate === null ||
isSameExpirationTime(
alternate.expirationTime_opaque,
(NoWork: ExpirationTimeOpaque),
))
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
@@ -1733,7 +1720,7 @@ function dispatchAction<S, A>(
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, lane);
}
}
@@ -12,7 +12,6 @@ import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Instance} from './ReactFiberHostConfig';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import {
flushSync,
@@ -21,7 +20,7 @@ import {
} from './ReactFiberWorkLoop.new';
import {updateContainer, syncUpdates} from './ReactFiberReconciler.new';
import {emptyContextObject} from './ReactFiberContext.new';
import {Sync} from './ReactFiberExpirationTime.new';
import {SyncLane} from './ReactFiberLane';
import {
ClassComponent,
FunctionComponent,
@@ -320,7 +319,7 @@ function scheduleFibersWithFamiliesRecursively(
fiber._debugNeedsRemount = true;
}
if (needsRemount || needsRender) {
scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
scheduleUpdateOnFiber(fiber, SyncLane);
}
if (child !== null && !needsRemount) {
scheduleFibersWithFamiliesRecursively(
@@ -55,7 +55,7 @@ import {
didNotFindHydratableSuspenseInstance,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {Never} from './ReactFiberExpirationTime.new';
import {OffscreenLane} from './ReactFiberLane';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -231,7 +231,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
retryTime: Never,
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;
// Store the dehydrated fragment as a child fiber.
+678 -12
View File
@@ -7,16 +7,682 @@
* @flow
*/
export opaque type LanePriority = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
import type {FiberRoot, ReactPriorityLevel} from './ReactInternalTypes';
export const SyncLanePriority: LanePriority = 10;
export const SyncBatchedLanePriority: LanePriority = 9;
export const InputDiscreteLanePriority: LanePriority = 8;
export const InputContinuousLanePriority: LanePriority = 7;
export const DefaultLanePriority: LanePriority = 6;
export const TransitionShortLanePriority: LanePriority = 5;
export const TransitionLongLanePriority: LanePriority = 4;
export const HydrationContinuousLanePriority: LanePriority = 3;
export const IdleLanePriority: LanePriority = 2;
export const OffscreenLanePriority: LanePriority = 1;
export const NoLanePriority: LanePriority = 0;
export opaque type LanePriority =
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
| 11
| 12
| 13
| 14
| 15
| 16;
export opaque type Lanes = number;
export opaque type Lane = number;
import invariant from 'shared/invariant';
import {
ImmediatePriority as ImmediateSchedulerPriority,
UserBlockingPriority as UserBlockingSchedulerPriority,
NormalPriority as NormalSchedulerPriority,
LowPriority as LowSchedulerPriority,
IdlePriority as IdleSchedulerPriority,
NoPriority as NoSchedulerPriority,
} from './SchedulerWithReactIntegration.new';
export const SyncLanePriority: LanePriority = 16;
const SyncBatchedLanePriority: LanePriority = 15;
const InputDiscreteHydrationLanePriority: LanePriority = 14;
export const InputDiscreteLanePriority: LanePriority = 13;
const InputContinuousHydrationLanePriority: LanePriority = 12;
const InputContinuousLanePriority: LanePriority = 11;
const DefaultHydrationLanePriority: LanePriority = 10;
const DefaultLanePriority: LanePriority = 9;
const TransitionShortHydrationLanePriority: LanePriority = 8;
export const TransitionShortLanePriority: LanePriority = 7;
const TransitionLongHydrationLanePriority: LanePriority = 6;
export const TransitionLongLanePriority: LanePriority = 5;
const SelectiveHydrationLanePriority: LanePriority = 4;
const IdleHydrationLanePriority: LanePriority = 3;
const IdleLanePriority: LanePriority = 2;
const OffscreenLanePriority: LanePriority = 1;
const NoLanePriority: LanePriority = 0;
const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
const SyncUpdateRangeEnd = 1;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;
const SyncBatchedUpdateRangeEnd = 2;
export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011100;
const InputDiscreteUpdateRangeStart = 3;
const InputDiscreteUpdateRangeEnd = 5;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011100000;
const InputContinuousUpdateRangeStart = 6;
const InputContinuousUpdateRangeEnd = 8;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
const DefaultLanes: Lanes = /* */ 0b0000000000000000011111100000000;
const DefaultUpdateRangeStart = 9;
const DefaultUpdateRangeEnd = 14;
const TransitionShortHydrationLane: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionShortLanes: Lanes = /* */ 0b0000000000011111100000000000000;
const TransitionShortUpdateRangeStart = 15;
const TransitionShortUpdateRangeEnd = 20;
const TransitionLongHydrationLane: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLongLanes: Lanes = /* */ 0b0000011111100000000000000000000;
const TransitionLongUpdateRangeStart = 21;
const TransitionLongUpdateRangeEnd = 26;
export const SelectiveHydrationLane: Lane = /* */ 0b0000110000000000000000000000000;
const SelectiveHydrationRangeEnd = 27;
// Includes all non-Idle updates
const UpdateRangeEnd = 27;
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /* */ 0b0111000000000000000000000000000;
const IdleUpdateRangeStart = 28;
const IdleUpdateRangeEnd = 30;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
// "Registers" used to "return" multiple values
// Used by getHighestPriorityLanes and getNextLanes:
let return_highestLanePriority: LanePriority = DefaultLanePriority;
let return_updateRangeEnd: number = -1;
function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
if ((SyncLane & lanes) !== NoLanes) {
return_highestLanePriority = SyncLanePriority;
return_updateRangeEnd = SyncUpdateRangeEnd;
return SyncLane;
}
if ((SyncBatchedLane & lanes) !== NoLanes) {
return_highestLanePriority = SyncBatchedLanePriority;
return_updateRangeEnd = SyncBatchedUpdateRangeEnd;
return SyncBatchedLane;
}
const inputDiscreteLanes = InputDiscreteLanes & lanes;
if (inputDiscreteLanes !== NoLanes) {
if (inputDiscreteLanes & InputDiscreteHydrationLane) {
return_highestLanePriority = InputDiscreteHydrationLanePriority;
return_updateRangeEnd = InputDiscreteUpdateRangeStart;
return InputDiscreteHydrationLane;
} else {
return_highestLanePriority = InputDiscreteLanePriority;
return_updateRangeEnd = InputDiscreteUpdateRangeEnd;
return inputDiscreteLanes;
}
}
const inputContinuousLanes = InputContinuousLanes & lanes;
if (inputContinuousLanes !== NoLanes) {
if (inputContinuousLanes & InputContinuousHydrationLane) {
return_highestLanePriority = InputContinuousHydrationLanePriority;
return_updateRangeEnd = InputContinuousUpdateRangeStart;
return InputContinuousHydrationLane;
} else {
return_highestLanePriority = InputContinuousLanePriority;
return_updateRangeEnd = InputContinuousUpdateRangeEnd;
return inputContinuousLanes;
}
}
const defaultLanes = DefaultLanes & lanes;
if (defaultLanes !== NoLanes) {
if (defaultLanes & DefaultHydrationLane) {
return_highestLanePriority = DefaultHydrationLanePriority;
return_updateRangeEnd = DefaultUpdateRangeStart;
return DefaultHydrationLane;
} else {
return_highestLanePriority = DefaultLanePriority;
return_updateRangeEnd = DefaultUpdateRangeEnd;
return defaultLanes;
}
}
const transitionShortLanes = TransitionShortLanes & lanes;
if (transitionShortLanes !== NoLanes) {
if (transitionShortLanes & TransitionShortHydrationLane) {
return_highestLanePriority = TransitionShortHydrationLanePriority;
return_updateRangeEnd = TransitionShortUpdateRangeStart;
return TransitionShortHydrationLane;
} else {
return_highestLanePriority = TransitionShortLanePriority;
return_updateRangeEnd = TransitionShortUpdateRangeEnd;
return transitionShortLanes;
}
}
const transitionLongLanes = TransitionLongLanes & lanes;
if (transitionLongLanes !== NoLanes) {
if (transitionLongLanes & TransitionLongHydrationLane) {
return_highestLanePriority = TransitionLongHydrationLanePriority;
return_updateRangeEnd = TransitionLongUpdateRangeStart;
return TransitionLongHydrationLane;
} else {
return_highestLanePriority = TransitionLongLanePriority;
return_updateRangeEnd = TransitionLongUpdateRangeEnd;
return transitionLongLanes;
}
}
if (lanes & SelectiveHydrationLane) {
return_highestLanePriority = SelectiveHydrationLanePriority;
return_updateRangeEnd = SelectiveHydrationRangeEnd;
return SelectiveHydrationLane;
}
const idleLanes = IdleLanes & lanes;
if (idleLanes !== NoLanes) {
if (idleLanes & IdleHydrationLane) {
return_highestLanePriority = IdleHydrationLanePriority;
return_updateRangeEnd = IdleUpdateRangeStart;
return IdleHydrationLane;
} else {
return_updateRangeEnd = IdleUpdateRangeEnd;
return idleLanes;
}
}
if ((OffscreenLane & lanes) !== NoLanes) {
return_highestLanePriority = OffscreenLanePriority;
return_updateRangeEnd = TotalLanes;
return OffscreenLane;
}
if (__DEV__) {
console.error('Should have found matching lanes. This is a bug in React.');
}
// This shouldn't be reachable, but as a fallback, return the entire bitmask.
return_highestLanePriority = DefaultLanePriority;
return_updateRangeEnd = DefaultUpdateRangeEnd;
return lanes;
}
export function schedulerPriorityToLanePriority(
schedulerPriorityLevel: ReactPriorityLevel,
): LanePriority {
switch (schedulerPriorityLevel) {
case ImmediateSchedulerPriority:
return SyncLanePriority;
case UserBlockingSchedulerPriority:
return InputContinuousLanePriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
// TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
return DefaultLanePriority;
case IdleSchedulerPriority:
return IdleLanePriority;
default:
return NoLanePriority;
}
}
export function lanePriorityToSchedulerPriority(
lanePriority: LanePriority,
): ReactPriorityLevel {
switch (lanePriority) {
case SyncLanePriority:
case SyncBatchedLanePriority:
return ImmediateSchedulerPriority;
case InputDiscreteHydrationLanePriority:
case InputDiscreteLanePriority:
case InputContinuousHydrationLanePriority:
case InputContinuousLanePriority:
return UserBlockingSchedulerPriority;
case DefaultHydrationLanePriority:
case DefaultLanePriority:
case TransitionShortHydrationLanePriority:
case TransitionShortLanePriority:
case TransitionLongHydrationLanePriority:
case TransitionLongLanePriority:
case SelectiveHydrationLanePriority:
return NormalSchedulerPriority;
case IdleHydrationLanePriority:
case IdleLanePriority:
case OffscreenLanePriority:
return IdleSchedulerPriority;
case NoLanePriority:
return NoSchedulerPriority;
default:
invariant(
false,
'Invalid update priority: %s. This is a bug in React.',
lanePriority,
);
}
}
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
// Early bailout if there's no pending work left.
const pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return_highestLanePriority = NoLanePriority;
return NoLanes;
}
let nextLanes = NoLanes;
let nextLanePriority = NoLanePriority;
let equalOrHigherPriorityLanes = NoLanes;
const expiredLanes = root.expiredLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
// Check if any work has expired.
if (expiredLanes !== NoLanes) {
nextLanes = expiredLanes;
nextLanePriority = return_highestLanePriority = SyncLanePriority;
equalOrHigherPriorityLanes = (getLowestPriorityLane(nextLanes) << 1) - 1;
} else {
// Do not work on any idle work until all the non-idle work has finished,
// even if the work is suspended.
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
} else {
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
}
}
} else {
// The only remaining work is Idle.
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
} else {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
nextLanePriority = return_highestLanePriority;
equalOrHigherPriorityLanes = (1 << return_updateRangeEnd) - 1;
}
}
}
}
if (nextLanes === NoLanes) {
// This should only be reachable if we're suspended
// TODO: Consider warning in this path if a fallback timer is not scheduled.
return NoLanes;
}
// If there are higher priority lanes, we'll include them even if they
// are suspended.
nextLanes = pendingLanes & equalOrHigherPriorityLanes;
// If we're already in the middle of a render, switching lanes will interrupt
// it and we'll lose our progress. We should only do this if the new lanes are
// higher priority.
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
// If we already suspended with a delay, then interrupting is fine. Don't
// bother waiting until the root is complete.
(wipLanes & suspendedLanes) === NoLanes
) {
getHighestPriorityLanes(wipLanes);
const wipLanePriority = return_highestLanePriority;
if (nextLanePriority <= wipLanePriority) {
return wipLanes;
} else {
return_highestLanePriority = nextLanePriority;
}
}
return nextLanes;
}
// This returns the highest priority pending lanes regardless of whether they
// are suspended.
export function getHighestPriorityPendingLanes(root: FiberRoot) {
return getHighestPriorityLanes(root.pendingLanes);
}
export function getLanesToRetrySynchronouslyOnError(root: FiberRoot): Lanes {
const everythingButOffscreen = root.pendingLanes & ~OffscreenLane;
if (everythingButOffscreen !== NoLanes) {
return everythingButOffscreen;
}
if (everythingButOffscreen & OffscreenLane) {
return OffscreenLane;
}
return NoLanes;
}
export function returnNextLanesPriority() {
return return_highestLanePriority;
}
export function hasUpdatePriority(lanes: Lanes) {
return (lanes & NonIdleLanes) !== NoLanes;
}
// To ensure consistency across multiple updates in the same event, this should
// be a pure function, so that it always returns the same lane for given inputs.
export function findUpdateLane(
lanePriority: LanePriority,
wipLanes: Lanes,
): Lane {
switch (lanePriority) {
case NoLanePriority:
break;
case SyncLanePriority:
return SyncLane;
case SyncBatchedLanePriority:
return SyncBatchedLane;
case InputDiscreteLanePriority: {
let lane = findLane(
InputDiscreteUpdateRangeStart,
UpdateRangeEnd,
wipLanes,
);
if (lane === NoLane) {
lane = InputDiscreteHydrationLane;
}
return lane;
}
case InputContinuousLanePriority: {
let lane = findLane(
InputContinuousUpdateRangeStart,
UpdateRangeEnd,
wipLanes,
);
if (lane === NoLane) {
lane = InputContinuousHydrationLane;
}
return lane;
}
case DefaultLanePriority: {
let lane = findLane(DefaultUpdateRangeStart, UpdateRangeEnd, wipLanes);
if (lane === NoLane) {
lane = DefaultHydrationLane;
}
return lane;
}
case TransitionShortLanePriority:
case TransitionLongLanePriority:
// Should be handled by findTransitionLane instead
break;
case IdleLanePriority:
let lane = findLane(IdleUpdateRangeStart, IdleUpdateRangeEnd, IdleLanes);
if (lane === NoLane) {
lane = IdleHydrationLane;
}
return lane;
default:
// The remaining priorities are not valid for updates
break;
}
invariant(
false,
'Invalid update priority: %s. This is a bug in React.',
lanePriority,
);
}
// To ensure consistency across multiple updates in the same event, this should
// be pure function, so that it always returns the same lane for given inputs.
export function findTransitionLane(
lanePriority: LanePriority,
wipLanes: Lanes,
pendingLanes: Lanes,
): Lane {
if (lanePriority === TransitionShortLanePriority) {
let lane = findLane(
TransitionShortUpdateRangeStart,
TransitionShortUpdateRangeEnd,
wipLanes | pendingLanes,
);
if (lane === NoLane) {
lane = findLane(
TransitionShortUpdateRangeStart,
TransitionShortUpdateRangeEnd,
wipLanes,
);
if (lane === NoLane) {
lane = TransitionShortHydrationLane;
}
}
return lane;
}
if (lanePriority === TransitionLongLanePriority) {
let lane = findLane(
TransitionLongUpdateRangeStart,
TransitionLongUpdateRangeEnd,
wipLanes | pendingLanes,
);
if (lane === NoLane) {
lane = findLane(
TransitionLongUpdateRangeStart,
TransitionLongUpdateRangeEnd,
wipLanes,
);
if (lane === NoLane) {
lane = TransitionLongHydrationLane;
}
}
return lane;
}
invariant(
false,
'Invalid transition priority: %s. This is a bug in React.',
lanePriority,
);
}
function findLane(start, end, skipLanes) {
// This finds the first bit between the `start` and `end` positions that isn't
// in `skipLanes`.
// TODO: This will always favor the rightmost bits. That's usually fine
// because any bit that's pending will be part of `skipLanes`, so we'll do our
// best to avoid accidental entanglement. However, lanes that are pending
// inside an Offscreen tree aren't considered "pending" at the root level. So
// they aren't included in `skipLanes`. So we should try not to favor any
// particular part of the range, perhaps by incrementing an offset for each
// distinct event. Must be the same within a single event, though.
const bitsInRange = ((1 << (end - start)) - 1) << start;
const possibleBits = bitsInRange & ~skipLanes;
const leastSignificantBit = possibleBits & -possibleBits;
return leastSignificantBit;
}
function getLowestPriorityLane(lanes: Lanes): Lane {
// This finds the most significant non-zero bit.
const index = 31 - clz32(lanes);
return index < 0 ? NoLanes : 1 << index;
}
export function pickArbitraryLane(lanes: Lanes): Lane {
return getLowestPriorityLane(lanes);
}
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
return (set & subset) === subset;
}
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
// Seems redundant, but it changes the type from a single lane (used for
// updates) to a group of lanes (used for flushing work).
export function laneToLanes(lane: Lane): Lanes {
return lane;
}
export function higherPriorityLane(a: Lane, b: Lane) {
// This works because the bit ranges decrease in priority as you go left.
return a !== NoLane && a < b ? a : b;
}
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
root.pendingLanes |= updateLane;
// TODO: Theoretically, any update to any lane can unblock any other lane. But
// it's not practical to try every single possible combination. We need a
// heuristic to decide which lanes to attempt to render, and in which batches.
// For now, we use the same heuristic as in the old ExpirationTimes model:
// retry any lane at equal or lower priority, but don't try updates at higher
// priority without also including the lower priority updates. This works well
// when considering updates across different priority levels, but isn't
// sufficient for updates within the same priority, since we want to treat
// those updates as parallel.
// Unsuspend any update at equal or lower priority.
const higherPriorityLanes = updateLane - 1; // Turns 0b1000 into 0b0111
root.suspendedLanes &= higherPriorityLanes;
root.pingedLanes &= higherPriorityLanes;
}
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
root.suspendedLanes |= suspendedLanes;
root.pingedLanes &= ~suspendedLanes;
}
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
}
export function markRootExpired(root: FiberRoot, expiredLanes: Lanes) {
root.expiredLanes |= expiredLanes & root.pendingLanes;
}
export function markDiscreteUpdatesExpired(root: FiberRoot) {
root.expiredLanes |= InputDiscreteLanes & root.pendingLanes;
}
export function hasDiscreteLanes(lanes: Lanes) {
return (lanes & InputDiscreteLanes) !== NoLanes;
}
export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
root.mutableReadLanes |= updateLane & root.pendingLanes;
}
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
root.pendingLanes = remainingLanes;
// Let's try everything again
root.suspendedLanes = 0;
root.pingedLanes = 0;
root.expiredLanes &= remainingLanes;
root.mutableReadLanes &= remainingLanes;
}
export function getBumpedLaneForHydration(
root: FiberRoot,
renderLanes: Lanes,
): Lane {
getHighestPriorityLanes(renderLanes);
const highestLanePriority = return_highestLanePriority;
let lane;
switch (highestLanePriority) {
case SyncLanePriority:
case SyncBatchedLanePriority:
lane = NoLane;
break;
case InputDiscreteHydrationLanePriority:
case InputDiscreteLanePriority:
lane = InputDiscreteHydrationLane;
break;
case InputContinuousHydrationLanePriority:
case InputContinuousLanePriority:
lane = InputContinuousHydrationLane;
break;
case DefaultHydrationLanePriority:
case DefaultLanePriority:
lane = DefaultHydrationLane;
break;
case TransitionShortHydrationLanePriority:
case TransitionShortLanePriority:
lane = TransitionShortHydrationLane;
break;
case TransitionLongHydrationLanePriority:
case TransitionLongLanePriority:
lane = TransitionLongHydrationLane;
break;
case SelectiveHydrationLanePriority:
lane = SelectiveHydrationLane;
break;
case IdleHydrationLanePriority:
case IdleLanePriority:
lane = IdleHydrationLane;
break;
case OffscreenLanePriority:
case NoLanePriority:
lane = NoLane;
break;
default:
invariant(false, 'Invalid lane: %s. This is a bug in React.', lane);
}
// Check if the lane we chose is suspended. If so, that indicates that we
// already attempted and failed to hydrate at that level. Also check if we're
// already rendering that lane, which is rare but could happen.
if ((lane & (root.suspendedLanes | renderLanes)) !== NoLane) {
// Give up trying to hydrate and fall back to client render.
return NoLane;
}
return lane;
}
const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
// Taken from:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
const log = Math.log;
const LN2 = Math.LN2;
function clz32Fallback(x) {
// Let n be ToUint32(x).
// Let p be the number of leading zero bits in
// the 32-bit binary representation of n.
// Return p.
const asUint = x >>> 0;
if (asUint === 0) {
return 32;
}
return (31 - ((log(asUint) / LN2) | 0)) | 0; // the "| 0" acts like math.floor
}
@@ -10,7 +10,7 @@
import type {ReactContext} from 'shared/ReactTypes';
import type {Fiber, ContextDependency} from './ReactInternalTypes';
import type {StackCursor} from './ReactFiberStack.new';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {createCursor, push, pop} from './ReactFiberStack.new';
@@ -20,12 +20,17 @@ import {
ClassComponent,
DehydratedFragment,
} from './ReactWorkTags';
import {isSameOrHigherPriority} from './ReactFiberExpirationTime.new';
import {
NoLanes,
isSubsetOfLanes,
includesSomeLane,
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane';
import invariant from 'shared/invariant';
import is from 'shared/objectIs';
import {createUpdate, enqueueUpdate, ForceUpdate} from './ReactUpdateQueue.new';
import {NoWork} from './ReactFiberExpirationTime.new';
import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork.new';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
@@ -148,37 +153,22 @@ export function calculateChangedBits<T>(
export function scheduleWorkOnParentPath(
parent: Fiber | null,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
) {
// Update the child expiration time of all the ancestors, including
// the alternates.
// Update the child lanes of all the ancestors, including the alternates.
let node = parent;
while (node !== null) {
const alternate = node.alternate;
if (
!isSameOrHigherPriority(
node.childExpirationTime_opaque,
renderExpirationTime,
)
) {
node.childExpirationTime_opaque = renderExpirationTime;
if (
alternate !== null &&
!isSameOrHigherPriority(
alternate.childExpirationTime_opaque,
renderExpirationTime,
)
) {
alternate.childExpirationTime_opaque = renderExpirationTime;
if (!isSubsetOfLanes(node.childLanes, renderLanes)) {
node.childLanes = mergeLanes(node.childLanes, renderLanes);
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
}
} else if (
alternate !== null &&
!isSameOrHigherPriority(
alternate.childExpirationTime_opaque,
renderExpirationTime,
)
!isSubsetOfLanes(alternate.childLanes, renderLanes)
) {
alternate.childExpirationTime_opaque = renderExpirationTime;
alternate.childLanes = mergeLanes(alternate.childLanes, renderLanes);
} else {
// Neither alternate was updated, which means the rest of the
// ancestor path already has sufficient priority.
@@ -192,7 +182,7 @@ export function propagateContextChange(
workInProgress: Fiber,
context: ReactContext<mixed>,
changedBits: number,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): void {
let fiber = workInProgress.child;
if (fiber !== null) {
@@ -218,7 +208,11 @@ export function propagateContextChange(
if (fiber.tag === ClassComponent) {
// Schedule a force update on the work-in-progress.
const update = createUpdate(-1, renderExpirationTime, null);
const update = createUpdate(
-1,
pickArbitraryLane(renderLanes),
null,
);
update.tag = ForceUpdate;
// TODO: Because we don't have a work-in-progress, this will add the
// update to the current fiber, too, which means it will persist even if
@@ -226,34 +220,15 @@ export function propagateContextChange(
// worth fixing.
enqueueUpdate(fiber, update);
}
if (
!isSameOrHigherPriority(
fiber.expirationTime_opaque,
renderExpirationTime,
)
) {
fiber.expirationTime_opaque = renderExpirationTime;
}
fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
const alternate = fiber.alternate;
if (
alternate !== null &&
!isSameOrHigherPriority(
alternate.expirationTime_opaque,
renderExpirationTime,
)
) {
alternate.expirationTime_opaque = renderExpirationTime;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
scheduleWorkOnParentPath(fiber.return, renderLanes);
scheduleWorkOnParentPath(fiber.return, renderExpirationTime);
// Mark the expiration time on the list, too.
if (
!isSameOrHigherPriority(list.expirationTime, renderExpirationTime)
) {
list.expirationTime = renderExpirationTime;
}
// Mark the updated lanes on the list, too.
list.lanes = mergeLanes(list.lanes, renderLanes);
// Since we already found a match, we can stop traversing the
// dependency list.
@@ -276,29 +251,16 @@ export function propagateContextChange(
parentSuspense !== null,
'We just came from a parent so we must have had a parent. This is a bug in React.',
);
if (
!isSameOrHigherPriority(
parentSuspense.expirationTime_opaque,
renderExpirationTime,
)
) {
parentSuspense.expirationTime_opaque = renderExpirationTime;
}
parentSuspense.lanes = mergeLanes(parentSuspense.lanes, renderLanes);
const alternate = parentSuspense.alternate;
if (
alternate !== null &&
!isSameOrHigherPriority(
alternate.expirationTime_opaque,
renderExpirationTime,
)
) {
alternate.expirationTime_opaque = renderExpirationTime;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
}
// This is intentionally passing this fiber as the parent
// because we want to schedule this fiber as having work
// on its children. We'll use the childExpirationTime on
// on its children. We'll use the childLanes on
// this fiber to indicate that a context has changed.
scheduleWorkOnParentPath(parentSuspense, renderExpirationTime);
scheduleWorkOnParentPath(parentSuspense, renderLanes);
nextFiber = fiber.sibling;
} else {
// Traverse down.
@@ -334,7 +296,7 @@ export function propagateContextChange(
export function prepareToReadContext(
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): void {
currentlyRenderingFiber = workInProgress;
lastContextDependency = null;
@@ -344,12 +306,7 @@ export function prepareToReadContext(
if (dependencies !== null) {
const firstContext = dependencies.firstContext;
if (firstContext !== null) {
if (
isSameOrHigherPriority(
dependencies.expirationTime,
renderExpirationTime,
)
) {
if (includesSomeLane(dependencies.lanes, renderLanes)) {
// Context list has a pending update. Mark that this fiber performed work.
markWorkInProgressReceivedUpdate();
}
@@ -411,7 +368,7 @@ export function readContext<T>(
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
currentlyRenderingFiber.dependencies_new = {
expirationTime: NoWork,
lanes: NoLanes,
firstContext: contextItem,
responders: null,
};
@@ -8,7 +8,7 @@
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
export type OffscreenProps = {|
// TODO: Pick an API before exposing the Offscreen type. I've chosen an enum
@@ -24,8 +24,8 @@ export type OffscreenProps = {|
// We use the existence of the state object as an indicator that the component
// is hidden.
export type OffscreenState = {|
// TODO: This doesn't do anything, yet. It's always NoWork. But eventually it
// TODO: This doesn't do anything, yet. It's always NoLanes. But eventually it
// will represent the pending work that must be included in the render in
// order to unhide the component.
baseTime: ExpirationTimeOpaque,
baseLanes: Lanes,
|};
@@ -19,7 +19,7 @@ import type {
import type {RendererInspectionConfig} from './ReactFiberHostConfig';
import {FundamentalComponent} from './ReactWorkTags';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lane} from './ReactFiberLane';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import {
@@ -47,7 +47,7 @@ import {createFiberRoot} from './ReactFiberRoot.new';
import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook.new';
import {
requestEventTime,
requestUpdateExpirationTime,
requestUpdateLane,
scheduleUpdateOnFiber,
flushRoot,
batchedEventUpdates,
@@ -74,11 +74,12 @@ import {
} from './ReactCurrentFiber';
import {StrictMode} from './ReactTypeOfMode';
import {
Sync,
ContinuousHydration,
UserBlockingUpdateTime,
isSameOrHigherPriority,
} from './ReactFiberExpirationTime.new';
SyncLane,
InputDiscreteHydrationLane,
SelectiveHydrationLane,
getHighestPriorityPendingLanes,
higherPriorityLane,
} from './ReactFiberLane';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
import {
scheduleRefresh,
@@ -235,7 +236,7 @@ export function updateContainer(
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTimeOpaque {
): Lane {
if (__DEV__) {
onScheduleRoot(container, element);
}
@@ -249,7 +250,7 @@ export function updateContainer(
}
}
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = requestUpdateExpirationTime(current, suspenseConfig);
const lane = requestUpdateLane(current, suspenseConfig);
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
@@ -275,7 +276,7 @@ export function updateContainer(
}
}
const update = createUpdate(eventTime, expirationTime, suspenseConfig);
const update = createUpdate(eventTime, lane, suspenseConfig);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
@@ -295,9 +296,9 @@ export function updateContainer(
}
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, expirationTime);
scheduleUpdateOnFiber(current, lane);
return expirationTime;
return lane;
}
export {
@@ -336,40 +337,37 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
const root: FiberRoot = fiber.stateNode;
if (root.hydrate) {
// Flush the first scheduled "update".
flushRoot(root, root.firstPendingTime_opaque);
const lanes = getHighestPriorityPendingLanes(root);
flushRoot(root, lanes);
}
break;
case SuspenseComponent:
flushSync(() =>
scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque)),
);
flushSync(() => scheduleUpdateOnFiber(fiber, SyncLane));
// If we're still blocked after this, we need to increase
// the priority of any promises resolving within this
// boundary so that they next attempt also has higher pri.
const retryExpTime = UserBlockingUpdateTime;
markRetryTimeIfNotHydrated(fiber, retryExpTime);
const retryLane = InputDiscreteHydrationLane;
markRetryLaneIfNotHydrated(fiber, retryLane);
break;
}
}
function markRetryTimeImpl(fiber: Fiber, retryTime: ExpirationTimeOpaque) {
function markRetryLaneImpl(fiber: Fiber, retryLane: Lane) {
const suspenseState: null | SuspenseState = fiber.memoizedState;
if (suspenseState !== null && suspenseState.dehydrated !== null) {
if (!isSameOrHigherPriority(suspenseState.retryTime, retryTime)) {
suspenseState.retryTime = retryTime;
}
suspenseState.retryLane = higherPriorityLane(
suspenseState.retryLane,
retryLane,
);
}
}
// Increases the priority of thennables when they resolve within this boundary.
function markRetryTimeIfNotHydrated(
fiber: Fiber,
retryTime: ExpirationTimeOpaque,
) {
markRetryTimeImpl(fiber, retryTime);
function markRetryLaneIfNotHydrated(fiber: Fiber, retryLane: Lane) {
markRetryLaneImpl(fiber, retryLane);
const alternate = fiber.alternate;
if (alternate) {
markRetryTimeImpl(alternate, retryTime);
markRetryLaneImpl(alternate, retryLane);
}
}
@@ -381,9 +379,9 @@ export function attemptUserBlockingHydration(fiber: Fiber): void {
// Suspense.
return;
}
const expTime = UserBlockingUpdateTime;
scheduleUpdateOnFiber(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
const lane = InputDiscreteHydrationLane;
scheduleUpdateOnFiber(fiber, lane);
markRetryLaneIfNotHydrated(fiber, lane);
}
export function attemptContinuousHydration(fiber: Fiber): void {
@@ -394,11 +392,9 @@ export function attemptContinuousHydration(fiber: Fiber): void {
// Suspense.
return;
}
scheduleUpdateOnFiber(fiber, (ContinuousHydration: ExpirationTimeOpaque));
markRetryTimeIfNotHydrated(
fiber,
(ContinuousHydration: ExpirationTimeOpaque),
);
const lane = SelectiveHydrationLane;
scheduleUpdateOnFiber(fiber, lane);
markRetryLaneIfNotHydrated(fiber, lane);
}
export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
@@ -407,9 +403,9 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
// their priority other than synchronously flush it.
return;
}
const expTime = requestUpdateExpirationTime(fiber, null);
scheduleUpdateOnFiber(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
const lane = requestUpdateLane(fiber, null);
scheduleUpdateOnFiber(fiber, lane);
markRetryLaneIfNotHydrated(fiber, lane);
}
export {findHostInstance};
@@ -491,7 +487,7 @@ if (__DEV__) {
// Shallow cloning props works as a workaround for now to bypass the bailout check.
fiber.memoizedProps = {...fiber.memoizedProps};
scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
scheduleUpdateOnFiber(fiber, SyncLane);
}
};
@@ -501,11 +497,11 @@ if (__DEV__) {
if (fiber.alternate) {
fiber.alternate.pendingProps = fiber.pendingProps;
}
scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
scheduleUpdateOnFiber(fiber, SyncLane);
};
scheduleUpdate = (fiber: Fiber) => {
scheduleUpdateOnFiber(fiber, (Sync: ExpirationTimeOpaque));
scheduleUpdateOnFiber(fiber, SyncLane);
};
setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
@@ -8,25 +8,17 @@
*/
import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {RootTag} from './ReactRootTags';
import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.new';
import {
NoWork,
isSameOrHigherPriority,
isSameExpirationTime,
bumpPriorityHigher,
bumpPriorityLower,
} from './ReactFiberExpirationTime.new';
import {NoLanes} from './ReactFiberLane';
import {
enableSchedulerTracing,
enableSuspenseCallback,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
import {initializeUpdateQueue} from './ReactUpdateQueue.new';
import {clearPendingUpdates as clearPendingMutableSourceUpdates} from './ReactMutableSource.new';
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
@@ -35,23 +27,22 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.current = null;
this.pingCache = null;
this.finishedWork = null;
this.finishedExpirationTime_opaque = NoWork;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.callbackNode = null;
this.callbackId = NoWork;
this.callbackId = NoLanes;
this.callbackIsSync = false;
this.expiresAt = -1;
this.firstPendingTime_opaque = NoWork;
this.lastPendingTime_opaque = NoWork;
this.firstSuspendedTime_opaque = NoWork;
this.lastSuspendedTime_opaque = NoWork;
this.nextKnownPendingLevel_opaque = NoWork;
this.lastPingedTime_opaque = NoWork;
this.lastExpiredTime_opaque = NoWork;
this.mutableSourceLastPendingUpdateTime_opaque = NoWork;
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
this.finishedLanes = NoLanes;
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
@@ -84,154 +75,3 @@ export function createFiberRoot(
return root;
}
export function isRootSuspendedAtTime(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): boolean {
const firstSuspendedTime = root.firstSuspendedTime_opaque;
const lastSuspendedTime = root.lastSuspendedTime_opaque;
return (
!isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque)) &&
isSameOrHigherPriority(firstSuspendedTime, expirationTime) &&
isSameOrHigherPriority(expirationTime, lastSuspendedTime)
);
}
export function markRootSuspendedAtTime(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): void {
const firstSuspendedTime = root.firstSuspendedTime_opaque;
const lastSuspendedTime = root.lastSuspendedTime_opaque;
if (!isSameOrHigherPriority(firstSuspendedTime, expirationTime)) {
root.firstSuspendedTime_opaque = expirationTime;
}
if (
!isSameOrHigherPriority(expirationTime, lastSuspendedTime) ||
isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque))
) {
root.lastSuspendedTime_opaque = expirationTime;
}
if (isSameOrHigherPriority(root.lastPingedTime_opaque, expirationTime)) {
root.lastPingedTime_opaque = NoWork;
}
if (isSameOrHigherPriority(root.lastExpiredTime_opaque, expirationTime)) {
root.lastExpiredTime_opaque = NoWork;
}
}
export function markRootUpdatedAtTime(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): void {
// Update the range of pending times
const firstPendingTime = root.firstPendingTime_opaque;
if (!isSameOrHigherPriority(firstPendingTime, expirationTime)) {
root.firstPendingTime_opaque = expirationTime;
}
const lastPendingTime = root.lastPendingTime_opaque;
if (
isSameExpirationTime(lastPendingTime, (NoWork: ExpirationTimeOpaque)) ||
!isSameOrHigherPriority(expirationTime, lastPendingTime)
) {
root.lastPendingTime_opaque = expirationTime;
}
// Update the range of suspended times. Treat everything lower priority or
// equal to this update as unsuspended.
const firstSuspendedTime = root.firstSuspendedTime_opaque;
if (
!isSameExpirationTime(firstSuspendedTime, (NoWork: ExpirationTimeOpaque))
) {
if (isSameOrHigherPriority(expirationTime, firstSuspendedTime)) {
// The entire suspended range is now unsuspended.
root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork;
} else if (
isSameOrHigherPriority(expirationTime, root.lastSuspendedTime_opaque)
) {
root.lastSuspendedTime_opaque = bumpPriorityHigher(expirationTime);
}
// This is a pending level. Check if it's higher priority than the next
// known pending level.
if (
!isSameOrHigherPriority(root.nextKnownPendingLevel_opaque, expirationTime)
) {
root.nextKnownPendingLevel_opaque = expirationTime;
}
}
}
export function markRootFinishedAtTime(
root: FiberRoot,
finishedExpirationTime: ExpirationTimeOpaque,
remainingExpirationTime: ExpirationTimeOpaque,
): void {
// Update the range of pending times
root.firstPendingTime_opaque = remainingExpirationTime;
if (
!isSameOrHigherPriority(
remainingExpirationTime,
root.lastPendingTime_opaque,
)
) {
// This usually means we've finished all the work, but it can also happen
// when something gets downprioritized during render, like a hidden tree.
root.lastPendingTime_opaque = remainingExpirationTime;
}
// Update the range of suspended times. Treat everything higher priority or
// equal to this update as unsuspended.
if (
isSameOrHigherPriority(
root.lastSuspendedTime_opaque,
finishedExpirationTime,
)
) {
// The entire suspended range is now unsuspended.
root.firstSuspendedTime_opaque = root.lastSuspendedTime_opaque = root.nextKnownPendingLevel_opaque = NoWork;
} else if (
isSameOrHigherPriority(
root.firstSuspendedTime_opaque,
finishedExpirationTime,
)
) {
// Part of the suspended range is now unsuspended. Narrow the range to
// include everything between the unsuspended time (non-inclusive) and the
// last suspended time.
root.firstSuspendedTime_opaque = bumpPriorityLower(finishedExpirationTime);
}
if (
isSameOrHigherPriority(root.lastPingedTime_opaque, finishedExpirationTime)
) {
// Clear the pinged time
root.lastPingedTime_opaque = NoWork;
}
if (
isSameOrHigherPriority(root.lastExpiredTime_opaque, finishedExpirationTime)
) {
// Clear the expired time
root.lastExpiredTime_opaque = NoWork;
}
// Clear any pending updates that were just processed.
clearPendingMutableSourceUpdates(root, finishedExpirationTime);
}
export function markRootExpiredAtTime(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): void {
const lastExpiredTime = root.lastExpiredTime_opaque;
if (
isSameExpirationTime(lastExpiredTime, (NoWork: ExpirationTimeOpaque)) ||
!isSameOrHigherPriority(expirationTime, lastExpiredTime)
) {
root.lastExpiredTime_opaque = expirationTime;
}
}
@@ -9,7 +9,7 @@
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lane} from './ReactFiberLane';
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoEffect, DidCapture} from './ReactSideEffectTags';
import {
@@ -29,11 +29,10 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
// Represents the earliest expiration time we should attempt to hydrate
// a dehydrated boundary at.
// Never is the default for dehydrated boundaries.
// NoWork is the default for normal boundaries, which turns into "normal" pri.
retryTime: ExpirationTimeOpaque,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
retryLane: Lane,
|};
export type SuspenseListTailMode = 'collapsed' | 'hidden' | void;
@@ -9,7 +9,7 @@
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lane, Lanes} from './ReactFiberLane';
import type {CapturedValue} from './ReactCapturedValue';
import type {Update} from './ReactUpdateQueue.new';
import type {Wakeable} from 'shared/ReactTypes';
@@ -54,16 +54,21 @@ import {
} from './ReactFiberWorkLoop.new';
import {logCapturedError} from './ReactFiberErrorLogger';
import {Sync, isSameExpirationTime} from './ReactFiberExpirationTime.new';
import {
SyncLane,
includesSomeLane,
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTimeOpaque,
lane: Lane,
): Update<mixed> {
const update = createUpdate(-1, expirationTime, null);
const update = createUpdate(-1, lane, null);
// Unmount the root by rendering null.
update.tag = CaptureUpdate;
// Caution: React DevTools currently depends on this property
@@ -80,9 +85,9 @@ function createRootErrorUpdate(
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTimeOpaque,
lane: Lane,
): Update<mixed> {
const update = createUpdate(-1, expirationTime, null);
const update = createUpdate(-1, lane, null);
update.tag = CaptureUpdate;
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === 'function') {
@@ -120,12 +125,7 @@ function createClassErrorUpdate(
// If componentDidCatch is the only error boundary method defined,
// then it needs to call setState to recover from errors.
// If no state update is scheduled then the boundary will swallow the error.
if (
!isSameExpirationTime(
fiber.expirationTime_opaque,
(Sync: ExpirationTimeOpaque),
)
) {
if (!includesSomeLane(fiber.lanes, (SyncLane: Lane))) {
console.error(
'%s: Error boundaries should implement getDerivedStateFromError(). ' +
'In that method, return a state update to display an error message or fallback UI.',
@@ -143,14 +143,10 @@ function createClassErrorUpdate(
return update;
}
function attachPingListener(
root: FiberRoot,
renderExpirationTime: ExpirationTimeOpaque,
wakeable: Wakeable,
) {
// Attach a listener to the promise to "ping" the root and retry. But
// only if one does not already exist for the current render expiration
// time (which acts like a "thread ID" here).
function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
// Attach a listener to the promise to "ping" the root and retry. But only if
// one does not already exist for the lanes we're currently rendering (which
// acts like a "thread ID" here).
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
@@ -164,15 +160,10 @@ function attachPingListener(
pingCache.set(wakeable, threadIDs);
}
}
if (!threadIDs.has(renderExpirationTime)) {
if (!threadIDs.has(lanes)) {
// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(renderExpirationTime);
const ping = pingSuspendedRoot.bind(
null,
root,
wakeable,
renderExpirationTime,
);
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
wakeable.then(ping, ping);
}
}
@@ -182,7 +173,7 @@ function throwException(
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
renderExpirationTime: ExpirationTimeOpaque,
rootRenderLanes: Lanes,
) {
// The source fiber did not complete.
sourceFiber.effectTag |= Incomplete;
@@ -204,7 +195,7 @@ function throwException(
if (currentSource) {
sourceFiber.updateQueue = currentSource.updateQueue;
sourceFiber.memoizedState = currentSource.memoizedState;
sourceFiber.expirationTime_opaque = currentSource.expirationTime_opaque;
sourceFiber.lanes = currentSource.lanes;
} else {
sourceFiber.updateQueue = null;
sourceFiber.memoizedState = null;
@@ -263,11 +254,7 @@ function throwException(
// When we try rendering again, we should not reuse the current fiber,
// since it's known to be in an inconsistent state. Use a force update to
// prevent a bail out.
const update = createUpdate(
-1,
(Sync: ExpirationTimeOpaque),
null,
);
const update = createUpdate(-1, SyncLane, null);
update.tag = ForceUpdate;
enqueueUpdate(sourceFiber, update);
}
@@ -275,7 +262,7 @@ function throwException(
// The source fiber did not complete. Mark it with Sync priority to
// indicate that it still has pending work.
sourceFiber.expirationTime_opaque = Sync;
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, SyncLane);
// Exit without suspending.
return;
@@ -323,10 +310,10 @@ function throwException(
// We want to ensure that a "busy" state doesn't get force committed. We want to
// ensure that new initial loading states can commit as soon as possible.
attachPingListener(root, renderExpirationTime, wakeable);
attachPingListener(root, wakeable, rootRenderLanes);
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime_opaque = renderExpirationTime;
workInProgress.lanes = rootRenderLanes;
return;
}
@@ -349,6 +336,7 @@ function throwException(
// over and traverse parent path again, this time treating the exception
// as an error.
renderDidError();
value = createCapturedValue(value, sourceFiber);
let workInProgress = returnFiber;
do {
@@ -356,12 +344,9 @@ function throwException(
case HostRoot: {
const errorInfo = value;
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime_opaque = renderExpirationTime;
const update = createRootErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
enqueueCapturedUpdate(workInProgress, update);
return;
}
@@ -378,12 +363,13 @@ function throwException(
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.effectTag |= ShouldCapture;
workInProgress.expirationTime_opaque = renderExpirationTime;
const lane = pickArbitraryLane(rootRenderLanes);
workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
lane,
);
enqueueCapturedUpdate(workInProgress, update);
return;
@@ -8,7 +8,7 @@
*/
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes} from './ReactFiberLane';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -35,14 +35,11 @@ import {
popTopLevelContextObject as popTopLevelLegacyContextObject,
} from './ReactFiberContext.new';
import {popProvider} from './ReactFiberNewContext.new';
import {popRenderExpirationTime} from './ReactFiberWorkLoop.new';
import {popRenderLanes} from './ReactFiberWorkLoop.new';
import invariant from 'shared/invariant';
function unwindWork(
workInProgress: Fiber,
renderExpirationTime: ExpirationTimeOpaque,
) {
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
@@ -110,7 +107,7 @@ function unwindWork(
return null;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderExpirationTime(workInProgress);
popRenderLanes(workInProgress);
return null;
default:
return null;
@@ -150,7 +147,7 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
break;
case OffscreenComponent:
case LegacyHiddenComponent:
popRenderExpirationTime(interruptedWork);
popRenderLanes(interruptedWork);
break;
default:
break;
File diff suppressed because it is too large Load Diff
+14 -22
View File
@@ -23,7 +23,7 @@ import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
import type {SideEffectTag} from './ReactSideEffectTags';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lane, Lanes} from './ReactFiberLane';
import type {HookType} from './ReactFiberHooks.old';
import type {RootTag} from './ReactRootTags';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
@@ -52,7 +52,7 @@ export type Dependencies_old = {
};
export type Dependencies_new = {
expirationTime: ExpirationTimeOpaque,
lanes: Lanes,
firstContext: ContextDependency<mixed> | null,
responders: Map<
ReactEventResponder<any, any>,
@@ -148,16 +148,13 @@ export type Fiber = {|
firstEffect: Fiber | null,
lastEffect: Fiber | null,
// Represents a time in the future by which this work should be completed.
// Does not include work found in its subtree.
// Only used by old reconciler
expirationTime: ExpirationTime,
// This is used to quickly determine if a subtree has no pending changes.
childExpirationTime: ExpirationTime,
// Only used by new reconciler
expirationTime_opaque: ExpirationTimeOpaque,
childExpirationTime_opaque: ExpirationTimeOpaque,
lanes: Lanes,
childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
@@ -255,8 +252,7 @@ type BaseFiberRootProperties = {|
// Represents the next task that the root should work on, or the current one
// if it's already working.
// TODO: In the new system, this will be a Lanes bitmask.
callbackId: ExpirationTimeOpaque,
callbackId: Lanes,
// Whether the currently scheduled task for this root is synchronous or
// batched/concurrent. We have to track this because Scheduler does not
// support synchronous tasks, so we put those on a separate queue. So you
@@ -269,17 +265,13 @@ type BaseFiberRootProperties = {|
// timestamp, in milliseconds.
expiresAt: number,
// Same as corresponding fields in the old reconciler, but opaque. These will
// become bitmasks.
finishedExpirationTime_opaque: ExpirationTimeOpaque,
firstPendingTime_opaque: ExpirationTimeOpaque,
lastPendingTime_opaque: ExpirationTimeOpaque,
firstSuspendedTime_opaque: ExpirationTimeOpaque,
lastSuspendedTime_opaque: ExpirationTimeOpaque,
nextKnownPendingLevel_opaque: ExpirationTimeOpaque,
lastPingedTime_opaque: ExpirationTimeOpaque,
lastExpiredTime_opaque: ExpirationTimeOpaque,
mutableSourceLastPendingUpdateTime_opaque: ExpirationTimeOpaque,
pendingLanes: Lanes,
suspendedLanes: Lanes,
pingedLanes: Lanes,
expiredLanes: Lanes,
mutableReadLanes: Lanes,
finishedLanes: Lanes,
|};
// The following attributes are only used by interaction tracing builds.
@@ -289,7 +281,7 @@ type BaseFiberRootProperties = {|
type ProfilingOnlyFiberRootProperties = {|
interactionThreadID: number,
memoizedInteractions: Set<Interaction>,
pendingInteractionMap_new: Map<ExpirationTimeOpaque, Set<Interaction>>,
pendingInteractionMap_new: Map<Lane | Lanes, Set<Interaction>>,
pendingInteractionMap_old: Map<ExpirationTime, Set<Interaction>>,
|};
@@ -7,16 +7,9 @@
* @flow
*/
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {FiberRoot} from './ReactInternalTypes';
import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {
NoWork,
isSameOrHigherPriority,
isSameExpirationTime,
} from './ReactFiberExpirationTime.new';
// Work in progress version numbers only apply to a single render,
// and should be reset before starting a new render.
@@ -30,44 +23,6 @@ if (__DEV__) {
rendererSigil = {};
}
export function clearPendingUpdates(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): void {
if (
isSameOrHigherPriority(
root.mutableSourceLastPendingUpdateTime_opaque,
expirationTime,
)
) {
// All updates for this source have been processed.
root.mutableSourceLastPendingUpdateTime_opaque = NoWork;
}
}
export function getLastPendingExpirationTime(
root: FiberRoot,
): ExpirationTimeOpaque {
return root.mutableSourceLastPendingUpdateTime_opaque;
}
export function setPendingExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTimeOpaque,
): void {
const mutableSourceLastPendingUpdateTime =
root.mutableSourceLastPendingUpdateTime_opaque;
if (
isSameExpirationTime(
mutableSourceLastPendingUpdateTime,
(NoWork: ExpirationTimeOpaque),
) ||
!isSameOrHigherPriority(expirationTime, mutableSourceLastPendingUpdateTime)
) {
root.mutableSourceLastPendingUpdateTime_opaque = expirationTime;
}
}
export function markSourceAsDirty(mutableSource: MutableSource<any>): void {
if (isPrimaryRenderer) {
workInProgressPrimarySources.push(mutableSource);
@@ -85,14 +85,10 @@
// resources, but the final state is always the same.
import type {Fiber} from './ReactInternalTypes';
import type {ExpirationTimeOpaque} from './ReactFiberExpirationTime.new';
import type {Lanes, Lane} from './ReactFiberLane';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import {
NoWork,
Sync,
isSameOrHigherPriority,
} from './ReactFiberExpirationTime.new';
import {NoLane, NoLanes, isSubsetOfLanes, mergeLanes} from './ReactFiberLane';
import {
enterDisallowedContextReadInDEV,
exitDisallowedContextReadInDEV,
@@ -104,7 +100,7 @@ import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags
import {StrictMode} from './ReactTypeOfMode';
import {
markRenderEventTimeAndConfig,
markUnprocessedUpdateTime,
markSkippedUpdateLanes,
} from './ReactFiberWorkLoop.new';
import invariant from 'shared/invariant';
@@ -115,7 +111,7 @@ export type Update<State> = {|
// TODO: Temporary field. Will remove this by storing a map of
// transition -> event time on the root.
eventTime: number,
expirationTime: ExpirationTimeOpaque,
lane: Lane,
suspenseConfig: null | SuspenseConfig,
tag: 0 | 1 | 2 | 3,
@@ -192,12 +188,12 @@ export function cloneUpdateQueue<State>(
export function createUpdate(
eventTime: number,
expirationTime: ExpirationTimeOpaque,
lane: Lane,
suspenseConfig: null | SuspenseConfig,
): Update<*> {
const update: Update<*> = {
eventTime,
expirationTime,
lane,
suspenseConfig,
tag: UpdateState,
@@ -272,7 +268,7 @@ export function enqueueCapturedUpdate<State>(
do {
const clone: Update<State> = {
eventTime: update.eventTime,
expirationTime: update.expirationTime,
lane: update.lane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
@@ -410,7 +406,7 @@ export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderExpirationTime: ExpirationTimeOpaque,
renderLanes: Lanes,
): void {
// This is always non-null on a ClassComponent or HostRoot
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
@@ -467,7 +463,9 @@ export function processUpdateQueue<State>(
if (firstBaseUpdate !== null) {
// Iterate through the list of updates to compute the result.
let newState = queue.baseState;
let newExpirationTime = NoWork;
// TODO: Don't need to accumulate this. Instead, we can remove renderLanes
// from the original lanes.
let newLanes = NoLanes;
let newBaseState = null;
let newFirstBaseUpdate = null;
@@ -475,15 +473,15 @@ export function processUpdateQueue<State>(
let update = firstBaseUpdate;
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;
const updateExpirationTime = update.expirationTime;
if (!isSameOrHigherPriority(updateExpirationTime, renderExpirationTime)) {
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
const clone: Update<State> = {
eventTime: updateEventTime,
expirationTime: updateExpirationTime,
lane: updateLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
@@ -499,16 +497,17 @@ export function processUpdateQueue<State>(
newLastBaseUpdate = newLastBaseUpdate.next = clone;
}
// Update the remaining priority in the queue.
if (!isSameOrHigherPriority(newExpirationTime, updateExpirationTime)) {
newExpirationTime = updateExpirationTime;
}
newLanes = mergeLanes(newLanes, updateLane);
} else {
// This update does have sufficient priority.
if (newLastBaseUpdate !== null) {
const clone: Update<State> = {
eventTime: updateEventTime,
expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
suspenseConfig: update.suspenseConfig,
tag: update.tag,
@@ -583,8 +582,8 @@ export function processUpdateQueue<State>(
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
markUnprocessedUpdateTime(newExpirationTime);
workInProgress.expirationTime_opaque = newExpirationTime;
markSkippedUpdateLanes(newLanes);
workInProgress.lanes = newLanes;
workInProgress.memoizedState = newState;
}
@@ -170,22 +170,32 @@ describe('ReactIncrementalUpdates', () => {
// Now flush the remaining work. Even though e and f were already processed,
// they should be processed again, to ensure that the terminal state
// is deterministic.
expect(Scheduler).toFlushAndYield([
'a',
'b',
'c',
expect(Scheduler).toFlushAndYield(
gate(flags =>
flags.new
? ['a', 'b', 'c', 'd', 'e', 'f', 'g']
: [
'a',
'b',
'c',
// e, f, and g are in a separate batch from a, b, and c because they
// were scheduled in the middle of a render
'e',
'f',
'g',
// The old reconciler has a quirk where `d` has slightly lower
// priority than `g`, because it was scheduled in the middle of a
// render. This is an implementation detail, but I've left the
// test in this branch as-is since this was written so long ago.
// This first render does not include d.
'e',
'f',
'g',
'd',
'e',
'f',
'g',
]);
// This second render does.
'd',
'e',
'f',
'g',
],
),
);
expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]);
});
@@ -244,22 +254,32 @@ describe('ReactIncrementalUpdates', () => {
// Now flush the remaining work. Even though e and f were already processed,
// they should be processed again, to ensure that the terminal state
// is deterministic.
expect(Scheduler).toFlushAndYield([
'a',
'b',
'c',
expect(Scheduler).toFlushAndYield(
gate(flags =>
flags.new
? ['a', 'b', 'c', 'd', 'e', 'f', 'g']
: [
'a',
'b',
'c',
// e, f, and g are in a separate batch from a, b, and c because they
// were scheduled in the middle of a render
'e',
'f',
'g',
// The old reconciler has a quirk where `d` has slightly lower
// priority than `g`, because it was scheduled in the middle of a
// render. This is an implementation detail, but I've left the
// test in this branch as-is since this was written so long ago.
// This first render does not include d.
'e',
'f',
'g',
'd',
'e',
'f',
'g',
]);
// This second render does.
'd',
'e',
'f',
'g',
],
),
);
expect(ReactNoop.getChildren()).toEqual([span('fg')]);
});
@@ -348,7 +368,7 @@ describe('ReactIncrementalUpdates', () => {
expect(Scheduler).toHaveYielded(['componentWillReceiveProps', 'render']);
});
it('enqueues setState inside an updater function as if the in-progress update is progressed (and warns)', () => {
it('updates triggered from inside a class setState updater', () => {
let instance;
class Foo extends React.Component {
state = {};
@@ -372,12 +392,26 @@ describe('ReactIncrementalUpdates', () => {
});
expect(() =>
expect(Scheduler).toFlushAndYield([
'setState updater',
// Update b is enqueued with the same priority as update a, so it should
// be flushed in the same commit.
'render',
]),
expect(Scheduler).toFlushAndYield(
gate(flags =>
flags.new
? [
'setState updater',
// In the new reconciler, updates inside the render phase are
// treated as if they came from an event, so the update gets
// shifted to a subsequent render.
'render',
'render',
]
: [
'setState updater',
// In the old reconciler, updates in the render phase receive
// the currently rendering expiration time, so the update
// flushes immediately in the same render.
'render',
],
),
),
).toErrorDev(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
@@ -391,7 +425,19 @@ describe('ReactIncrementalUpdates', () => {
this.setState({a: 'a'});
return {b: 'b'};
});
expect(Scheduler).toFlushAndYield(['render']);
expect(Scheduler).toFlushAndYield(
gate(flags =>
flags.new
? // In the new reconciler, updates inside the render phase are
// treated as if they came from an event, so the update gets shifted
// to a subsequent render.
['render', 'render']
: // In the old reconciler, updates in the render phase receive
// the currently rendering expiration time, so the update flushes
// immediately in the same render.
['render'],
),
);
});
it('getDerivedStateFromProps should update base state of updateQueue (based on product bug)', () => {
@@ -322,6 +322,7 @@ describe('ReactSuspense', () => {
},
);
// @gate experimental
it(
'interrupts current render when something suspends with a ' +
"delay and we've already skipped over a lower priority update in " +
@@ -360,9 +361,14 @@ describe('ReactSuspense', () => {
// Do a bit of work
expect(Scheduler).toFlushAndYieldThrough(['A1']);
// Schedule another update. This will get bumped into a different batch
// because we're already in the middle of rendering.
root.update(<App shouldSuspend={false} step={2} />);
// Schedule another update. This will have lower priority because it's
// a transition.
React.unstable_withSuspenseConfig(
() => {
root.update(<App shouldSuspend={false} step={2} />);
},
{timeoutMs: 10000},
);
// Interrupt to trigger a restart.
interrupt();
@@ -389,6 +395,7 @@ describe('ReactSuspense', () => {
},
);
// @gate experimental
it(
'interrupts current render when something suspends with a ' +
"delay and we've already bailed out lower priority update in " +
@@ -450,16 +457,20 @@ describe('ReactSuspense', () => {
setShouldSuspend(true);
// Need to move into the next async bucket.
Scheduler.unstable_advanceTime(1000);
// Do a bit of work, then interrupt to trigger a restart.
expect(Scheduler).toFlushAndYieldThrough(['A']);
interrupt();
// Should not have committed loading state
expect(root).toMatchRenderedOutput('ABC');
// Schedule another update. This will have lower priority because of
// the interrupt trick above.
setShouldHideInParent(true);
// Schedule another update. This will have lower priority because it's
// a transition.
React.unstable_withSuspenseConfig(
() => {
setShouldHideInParent(true);
},
{timeoutMs: 10000},
);
expect(Scheduler).toFlushAndYieldThrough([
// Should have restarted the first update, because of the interruption
@@ -31,23 +31,20 @@ describe('ReactSuspense', () => {
function createThenable() {
let completed = false;
const resolveRef = {current: null};
const promise = {
then(resolve, reject) {
resolveRef.current = () => {
completed = true;
resolve();
};
},
};
let resolve;
const promise = new Promise(res => {
resolve = () => {
completed = true;
res();
};
});
const PromiseComp = () => {
if (!completed) {
throw promise;
}
return 'Done';
};
return {promise, resolveRef, PromiseComp};
return {promise, resolve, PromiseComp};
}
it('check type', () => {
@@ -74,8 +71,8 @@ describe('ReactSuspense', () => {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([]);
});
it('1 then 0 suspense callback', () => {
const {promise, resolveRef, PromiseComp} = createThenable();
it('1 then 0 suspense callback', async () => {
const {promise, resolve, PromiseComp} = createThenable();
let ops = [];
const suspenseCallback = thenables => {
@@ -94,21 +91,21 @@ describe('ReactSuspense', () => {
expect(ops).toEqual([new Set([promise])]);
ops = [];
resolveRef.current();
await resolve();
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([text('Done')]);
expect(ops).toEqual([]);
});
it('2 then 1 then 0 suspense callback', () => {
it('2 then 1 then 0 suspense callback', async () => {
const {
promise: promise1,
resolveRef: resolveRef1,
resolve: resolve1,
PromiseComp: PromiseComp1,
} = createThenable();
const {
promise: promise2,
resolveRef: resolveRef2,
resolve: resolve2,
PromiseComp: PromiseComp2,
} = createThenable();
@@ -132,14 +129,14 @@ describe('ReactSuspense', () => {
expect(ops).toEqual([new Set([promise1, promise2])]);
ops = [];
resolveRef1.current();
await resolve1();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 1')]);
expect(ops).toEqual([new Set([promise2])]);
ops = [];
resolveRef2.current();
await resolve2();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]);
@@ -177,15 +174,15 @@ describe('ReactSuspense', () => {
expect(ops2).toEqual([new Set([promise])]);
});
it('competing suspense promises', () => {
it('competing suspense promises', async () => {
const {
promise: promise1,
resolveRef: resolveRef1,
resolve: resolve1,
PromiseComp: PromiseComp1,
} = createThenable();
const {
promise: promise2,
resolveRef: resolveRef2,
resolve: resolve2,
PromiseComp: PromiseComp2,
} = createThenable();
@@ -219,7 +216,7 @@ describe('ReactSuspense', () => {
ops1 = [];
ops2 = [];
resolveRef1.current();
await resolve1();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([
@@ -231,7 +228,7 @@ describe('ReactSuspense', () => {
ops1 = [];
ops2 = [];
resolveRef2.current();
await resolve2();
ReactNoop.render(element);
expect(Scheduler).toFlushWithoutYielding();
expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]);
+4 -1
View File
@@ -354,5 +354,8 @@
"354": "getInspectorDataForViewAtPoint() is not available in production.",
"355": "The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. Do not read the value directly.",
"356": "Could not read the cache.",
"357": "The current renderer does not support React Scopes. This error is likely caused by a bug in React. Please file an issue."
"357": "The current renderer does not support React Scopes. This error is likely caused by a bug in React. Please file an issue.",
"358": "Invalid update priority: %s. This is a bug in React.",
"359": "Invalid transition priority: %s. This is a bug in React.",
"360": "Invalid lane: %s. This is a bug in React."
}