Rewrite the Suspense logic

This commit is contained in:
Dan
2019-04-05 23:12:57 +01:00
parent d6257d382d
commit 0f536bba5c
3 changed files with 209 additions and 100 deletions
+157 -96
View File
@@ -22,6 +22,7 @@ import {
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_RESET_CHILDREN,
TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
} from '../constants';
import { getUID } from '../utils';
@@ -627,7 +628,7 @@ export function attach(
pendingOperations = new Uint32Array(0);
}
function enqueueMount(fiber: Fiber, parentFiber: Fiber | null) {
function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
const isRoot = fiber.tag === HostRoot;
const id = getFiberID(getPrimaryFiber(fiber));
@@ -713,7 +714,7 @@ export function attach(
}
}
function enqueueUnmount(fiber) {
function recordUnmount(fiber) {
const isRoot = fiber.tag === HostRoot;
const primaryFiber = getPrimaryFiber(fiber);
if (!fiberToIDMap.has(primaryFiber)) {
@@ -753,18 +754,27 @@ export function attach(
}
}
function mountFiber(
function recordRecursiveRemoveChildren(fiber) {
const primaryFiber = getPrimaryFiber(fiber);
const id = getFiberID(primaryFiber);
const operation = new Uint32Array(2);
operation[0] = TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN;
operation[1] = id;
addOperation(operation, false);
}
function mountFiberRecursively(
fiber: Fiber,
parentFiber: Fiber | null,
traverseSiblings = false
) {
if (__DEBUG__) {
debug('mountFiber()', fiber, parentFiber);
debug('mountFiberRecursively()', fiber, parentFiber);
}
const shouldEnqueueMount = !shouldFilterFiber(fiber);
if (shouldEnqueueMount) {
enqueueMount(fiber, parentFiber);
const shouldInclude = !shouldFilterFiber(fiber);
if (shouldInclude) {
recordMount(fiber, parentFiber);
}
const isTimedOutSuspense =
@@ -779,25 +789,45 @@ export function attach(
const fallbackChildFragment = primaryChildFragment.sibling;
const fallbackChild = fallbackChildFragment.child;
if (fallbackChild !== null) {
mountFiber(fallbackChild, shouldEnqueueMount ? fiber : parentFiber, true);
mountFiberRecursively(
fallbackChild,
shouldInclude ? fiber : parentFiber,
true
);
}
} else {
if (fiber.child !== null) {
mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber, true);
mountFiberRecursively(
fiber.child,
shouldInclude ? fiber : parentFiber,
true
);
}
}
if (traverseSiblings && fiber.sibling !== null) {
mountFiber(fiber.sibling, parentFiber, true);
mountFiberRecursively(fiber.sibling, parentFiber, true);
}
}
function enqueueUpdateIfNecessary(
fiber: Fiber,
hasChildOrderChanged: boolean
) {
function unmountFiberRecursively(fiber, traverseSiblings = false) {
if (__DEBUG__) {
debug('enqueueUpdateIfNecessary()', fiber);
debug('unmountFiberRecursively()', fiber, traverseSiblings);
}
if (!shouldFilterFiber(fiber)) {
recordUnmount(fiber);
}
if (fiber.child !== null) {
unmountFiberRecursively(fiber.child, true);
}
if (traverseSiblings && fiber.sibling !== null) {
unmountFiberRecursively(fiber.sibling, true);
}
}
function maybeRecordUpdate(fiber: Fiber, hasChildOrderChanged: boolean) {
if (__DEBUG__) {
debug('maybeRecordUpdate()', fiber);
}
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
@@ -844,7 +874,7 @@ export function attach(
// We might want to revisit this if it proves to be too inefficient.
let child = fiber.child;
while (child !== null) {
findReorderedChildren(child, nextChildren);
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
}
@@ -858,109 +888,140 @@ export function attach(
}
}
function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
function findReorderedChildrenRecursively(
fiber: Fiber,
nextChildren: Array<number>
) {
if (!shouldFilterFiber(fiber)) {
nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
} else {
let child = fiber.child;
while (child !== null) {
findReorderedChildren(child, nextChildren);
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
}
}
}
function updateFiber(
function updateFiberRecursively(
nextFiber: Fiber,
prevFiber: Fiber,
parentFiber: Fiber | null
) {
if (__DEBUG__) {
debug('enqueueUpdateIfNecessary()', nextFiber, parentFiber);
debug('updateFiberRecursively()', nextFiber, parentFiber);
}
const shouldEnqueueUpdate = !shouldFilterFiber(nextFiber);
// The behavior of timed-out Suspense trees is unique.
// Rather than unmount the timed out content (and possibly lose important state),
// React re-parents this content within a hidden Fragment while the fallback is showing.
// This behavior doesn't need to be observable in the DevTools though.
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
// The easiest fix is to strip out the intermediate Fragment fibers,
// so the Elements panel and Profiler don't need to special case them.
if (nextFiber.tag === SuspenseComponent) {
// Suspense components only have a non-null memoizedState if they're timed-out.
const prevDidTimeout = prevFiber.memoizedState !== null;
const nextDidTimeOut = nextFiber.memoizedState !== null;
// Suspense components only have a non-null memoizedState if they're timed-out.
const isTimedOutSuspense =
nextFiber.tag === SuspenseComponent && nextFiber.memoizedState !== null;
if (isTimedOutSuspense) {
// The behavior of timed-out Suspense trees is unique.
// Rather than unmount the timed out content (and possibly lose important state),
// React re-parents this content within a hidden Fragment while the fallback is showing.
// This behavior doesn't need to be observable in the DevTools though.
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
// The easiest fix is to strip out the intermediate Fragment fibers,
// so the Elements panel and Profiler don't need to special case them.
const primaryChildFragment = nextFiber.child;
const fallbackChildFragment = primaryChildFragment.sibling;
const fallbackChild = fallbackChildFragment.child;
// The primary, hidden child is never actually updated in this case,
// so we can skip any updates to its tree.
// We only need to track updates to the Fallback UI for now.
if (fallbackChild.alternate) {
updateFiber(fallbackChild, fallbackChild.alternate, nextFiber);
// The logic below is inspired by the codepaths in updateSuspenseComponent()
// inside ReactFiberBeginWork in the React source code.
if (prevDidTimeout) {
if (nextDidTimeOut) {
// Fallback -> Fallback:
// 1. Reconcile fallback set.
const nextFallbackChildSet = nextFiber.child.sibling;
// Note: We can't use nextFiber.child.sibling.alternate
// because the set is special and alternate may not exist.
const prevFallbackChildSet = prevFiber.child.sibling;
updateFiberRecursively(
nextFallbackChildSet,
prevFallbackChildSet,
nextFiber
);
return;
} else {
// Fallback -> Primary:
// 1. Unmount fallback set
// Note: don't emulate fallback unmount because React actually did it.
// 2. Mount primary set
const nextPrimaryChildSet = nextFiber.child;
mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
return;
}
} else {
mountFiber(fallbackChild, nextFiber);
if (nextDidTimeOut) {
// Primary -> Fallback:
// 1. Hide primary set
// This is not a real unmount, so it won't get reported by React.
// By this point it's *too late* to find the previous primary child set
// so we'll just tell the store to "forget" about those children.
// They might "resurface" later when we switch to primary content,
// but from the store's point of view they will be a new tree.
recordRecursiveRemoveChildren(nextFiber);
// 2. Mount fallback set
const nextFallbackChildSet = nextFiber.child.sibling;
mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
return;
} else {
// Primary -> Primary:
// 1. Reconcile primary set.
// Note: no return so we can passthrough to the logic below.
}
}
}
if (shouldEnqueueUpdate) {
enqueueUpdateIfNecessary(nextFiber, false);
}
} else {
let hasChildOrderChanged = false;
if (nextFiber.child !== prevFiber.child) {
// If the first child is different, we need to traverse them.
// Each next child will be either a new child (mount) or an alternate (update).
let nextChild = nextFiber.child;
let prevChildAtSameIndex = prevFiber.child;
while (nextChild) {
// We already know children will be referentially different because
// they are either new mounts or alternates of previous children.
// Schedule updates and mounts depending on whether alternates exist.
// We don't track deletions here because they are reported separately.
if (nextChild.alternate) {
const prevChild = nextChild.alternate;
updateFiber(
nextChild,
prevChild,
shouldEnqueueUpdate ? nextFiber : parentFiber
);
// However we also keep track if the order of the children matches
// the previous order. They are always different referentially, but
// if the instances line up conceptually we'll want to know that.
if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
hasChildOrderChanged = true;
}
} else {
mountFiber(
nextChild,
shouldEnqueueUpdate ? nextFiber : parentFiber
);
if (!hasChildOrderChanged) {
hasChildOrderChanged = true;
}
const shouldInclude = !shouldFilterFiber(nextFiber);
let hasChildOrderChanged = false;
if (nextFiber.child !== prevFiber.child) {
// If the first child is different, we need to traverse them.
// Each next child will be either a new child (mount) or an alternate (update).
let nextChild = nextFiber.child;
let prevChildAtSameIndex = prevFiber.child;
while (nextChild) {
// We already know children will be referentially different because
// they are either new mounts or alternates of previous children.
// Schedule updates and mounts depending on whether alternates exist.
// We don't track deletions here because they are reported separately.
if (nextChild.alternate) {
const prevChild = nextChild.alternate;
updateFiberRecursively(
nextChild,
prevChild,
shouldInclude ? nextFiber : parentFiber
);
// However we also keep track if the order of the children matches
// the previous order. They are always different referentially, but
// if the instances line up conceptually we'll want to know that.
if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
hasChildOrderChanged = true;
}
// Try the next child.
nextChild = nextChild.sibling;
// Advance the pointer in the previous list so that we can
// keep comparing if they line up.
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
} else {
mountFiberRecursively(
nextChild,
shouldInclude ? nextFiber : parentFiber
);
if (!hasChildOrderChanged) {
hasChildOrderChanged = true;
}
}
// If we have no more children, but used to, they don't line up.
// Try the next child.
nextChild = nextChild.sibling;
// Advance the pointer in the previous list so that we can
// keep comparing if they line up.
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
hasChildOrderChanged = true;
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
}
}
if (shouldEnqueueUpdate) {
enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
// If we have no more children, but used to, they don't line up.
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
hasChildOrderChanged = true;
}
}
if (shouldInclude) {
maybeRecordUpdate(nextFiber, hasChildOrderChanged);
}
}
function cleanup() {
@@ -1002,7 +1063,7 @@ export function attach(
};
}
mountFiber(root.current, null);
mountFiberRecursively(root.current, null);
flushPendingEvents(root);
currentRootID = -1;
});
@@ -1013,7 +1074,7 @@ export function attach(
// This is not recursive.
// We can't traverse fibers after unmounting so instead
// we rely on React telling us about each unmount.
enqueueUnmount(fiber);
recordUnmount(fiber);
}
function handleCommitFiberRoot(root) {
@@ -1047,17 +1108,17 @@ export function attach(
current.memoizedState != null && current.memoizedState.element != null;
if (!wasMounted && isMounted) {
// Mount a new root.
mountFiber(current, null);
mountFiberRecursively(current, null);
} else if (wasMounted && isMounted) {
// Update an existing root.
updateFiber(current, alternate, null);
updateFiberRecursively(current, alternate, null);
} else if (wasMounted && !isMounted) {
// Unmount an existing root.
enqueueUnmount(current);
recordUnmount(current);
}
} else {
// Mount a new root.
mountFiber(current, null);
mountFiberRecursively(current, null);
}
if (isProfiling) {
+1
View File
@@ -4,6 +4,7 @@ export const TREE_OPERATION_ADD = 1;
export const TREE_OPERATION_REMOVE = 2;
export const TREE_OPERATION_RESET_CHILDREN = 3;
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
export const TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN = 5;
export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
'React::DevTools::reloadAndProfile';
+51 -4
View File
@@ -3,6 +3,7 @@
import EventEmitter from 'events';
import {
TREE_OPERATION_ADD,
TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN,
TREE_OPERATION_REMOVE,
TREE_OPERATION_RESET_CHILDREN,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
@@ -572,7 +573,52 @@ export default class Store extends EventEmitter {
weightDelta = 1;
}
break;
case TREE_OPERATION_REMOVE:
case TREE_OPERATION_RECURSIVE_REMOVE_CHILDREN: {
id = ((operations[i + 1]: any): number);
if (!this._idToElement.has(id)) {
throw new Error(
'Store does not contain fiber ' +
id +
'. This is a bug in React DevTools.'
);
}
i = i + 2;
let justRemovedIDs = [];
const recursivelyRemove = childID => {
justRemovedIDs.push(childID);
const child = this._idToElement.get(childID);
if (!child) {
throw new Error(
'Store does not contain fiber ' +
childID +
'. This is a bug in React DevTools.'
);
}
this._idToElement.delete(childID);
child.children.forEach(recursivelyRemove);
};
// Track removed items so search results can be updated
const oldRemovedElementIDs = removedElementIDs;
removedElementIDs = new Uint32Array(
removedElementIDs.length + justRemovedIDs.length
);
removedElementIDs.set(oldRemovedElementIDs);
let startIndex = oldRemovedElementIDs.length;
for (let j = 0; j < justRemovedIDs.length; j++) {
removedElementIDs[startIndex + j] = oldRemovedElementIDs[j];
}
parentElement = ((this._idToElement.get(id): any): Element);
parentElement.children.forEach(recursivelyRemove);
parentElement.children = [];
weightDelta = -parentElement.weight + 1;
break;
}
case TREE_OPERATION_REMOVE: {
id = ((operations[i + 1]: any): number);
if (!this._idToElement.has(id)) {
@@ -614,11 +660,12 @@ export default class Store extends EventEmitter {
}
// Track removed items so search results can be updated
const oldRemovededElementIDs = removedElementIDs;
const oldRemovedElementIDs = removedElementIDs;
removedElementIDs = new Uint32Array(removedElementIDs.length + 1);
removedElementIDs.set(oldRemovededElementIDs);
removedElementIDs[oldRemovededElementIDs.length] = id;
removedElementIDs.set(oldRemovedElementIDs);
removedElementIDs[oldRemovedElementIDs.length] = id;
break;
}
case TREE_OPERATION_RESET_CHILDREN:
id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);