Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored backend renderer to remove most of the recursion #16627

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 126 additions & 73 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,9 @@ export function attach(
}
}

// TRICKY
// This method recursively mounts a fiber, but it does so iteratively.
// This is to avoid hitting the call stack limit for extremely deep or wide trees.
function mountFiberRecursively(
fiber: Fiber,
parentFiber: Fiber | null,
Expand All @@ -1236,89 +1239,126 @@ export function attach(
debug('mountFiberRecursively()', fiber, parentFiber);
}

// If we have the tree selection from previous reload, try to match this Fiber.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
fiber,
);
const stack = [fiber, parentFiber, traverseSiblings];

const shouldIncludeInTree = !shouldFilterFiber(fiber);
if (shouldIncludeInTree) {
recordMount(fiber, parentFiber);
}
while (stack.length > 0) {
const currentFiber = ((stack.shift(): any): Fiber);
const currentParentFiber = ((stack.shift(): any): Fiber | null);
const currentTraverseSiblings = ((stack.shift(): any): boolean);

const isTimedOutSuspense =
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
fiber.memoizedState !== null;

if (isTimedOutSuspense) {
// Special case: if Suspense mounts in a timed-out state,
// get the fallback child from the inner fragment and mount
// it as if it was our own child. Updates handle this too.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
mountFiberRecursively(
fallbackChild,
shouldIncludeInTree ? fiber : parentFiber,
true,
);
// If we have the tree selection from previous reload, try to match this Fiber.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath = updateTrackedPathStateBeforeMount(
currentFiber,
);

const shouldIncludeInTree = !shouldFilterFiber(currentFiber);
if (shouldIncludeInTree) {
recordMount(currentFiber, currentParentFiber);
}
} else {
if (fiber.child !== null) {
mountFiberRecursively(
fiber.child,
shouldIncludeInTree ? fiber : parentFiber,
true,
);

const isTimedOutSuspense =
currentFiber.tag === ReactTypeOfWork.SuspenseComponent &&
currentFiber.memoizedState !== null;

let didQueueChild = false;

if (isTimedOutSuspense) {
// Special case: if Suspense mounts in a timed-out state,
// get the fallback child from the inner fragment and mount
// it as if it was our own child. Updates handle this too.
const primaryChildFragment = currentFiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
didQueueChild = true;
stack.unshift(
fallbackChild,
shouldIncludeInTree ? currentFiber : currentParentFiber,
true,
);
}
} else {
if (currentFiber.child !== null) {
didQueueChild = true;
stack.unshift(
currentFiber.child,
shouldIncludeInTree ? currentFiber : currentParentFiber,
true,
);
}
}
}

// We're exiting this Fiber now, and entering its siblings.
// If we have selection to restore, we might need to re-activate tracking.
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
// We're exiting this Fiber now, and entering its siblings.
// If we have selection to restore, we might need to re-activate tracking.
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);

if (traverseSiblings && fiber.sibling !== null) {
mountFiberRecursively(fiber.sibling, parentFiber, true);
if (currentTraverseSiblings && currentFiber.sibling !== null) {
// Siblings should be crawled after children, but before ancestors.
stack.splice(
didQueueChild ? 3 : 0,
0,
currentFiber.sibling,
currentParentFiber,
true,
);
}
}
}

// We use this to simulate unmounting for Suspense trees
// when we switch from primary to fallback.
// We use this to simulate unmounting for Suspense trees when we switch from primary to fallback.
//
// TRICKY
// This method recursively unmounts a fiber, but it does so iteratively.
// This is to avoid hitting the call stack limit for extremely deep or wide trees.
function unmountFiberChildrenRecursively(fiber: Fiber) {
if (__DEBUG__) {
debug('unmountFiberChildrenRecursively()', fiber);
}

// We might meet a nested Suspense on our way.
const isTimedOutSuspense =
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
fiber.memoizedState !== null;
const fibers: Array<Fiber> = [fiber];
let index = 0;

let child = fiber.child;
if (isTimedOutSuspense) {
// If it's showing fallback tree, let's traverse it instead.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
// Skip over to the real Fiber child.
child = fallbackChildFragment ? fallbackChildFragment.child : null;
}
// Fibers must be unmounted before their parents.
// So first we crawl the tree to determine which fibers need to be unmounted,
// then we step backwards to unmount them.
for (index = 0; index < fibers.length; index++) {
const currentFiber = fibers[index];

while (child !== null) {
// Record simulated unmounts children-first.
// We skip nodes without return because those are real unmounts.
if (child.return !== null) {
unmountFiberChildrenRecursively(child);
recordUnmount(child, true);
// We might meet a nested Suspense on our way.
const isTimedOutSuspense =
currentFiber.tag === ReactTypeOfWork.SuspenseComponent &&
currentFiber.memoizedState !== null;

let child = currentFiber.child;
if (isTimedOutSuspense) {
// If it's showing fallback tree, let's traverse it instead.
const primaryChildFragment = currentFiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
// Skip over to the real Fiber child.
child = fallbackChildFragment ? fallbackChildFragment.child : null;
}
child = child.sibling;

while (child !== null) {
// Record simulated unmounts children-first.
// We skip nodes without return because those are real unmounts.
if (child.return !== null) {
fibers.push(child);
}
child = child.sibling;
}
}

// Note that this method doesn't unmount the original fiber, only its children.
for (index = fibers.length - 1; index > 0; index--) {
const currentFiber = fibers[index];
recordUnmount(currentFiber, true);
}
}

Expand Down Expand Up @@ -1413,22 +1453,35 @@ export function attach(
}
}

// TRICKY
// This method recursively updates child order, but it does so iteratively.
// This is to avoid hitting the call stack limit for extremely deep or wide trees.
function findReorderedChildrenRecursively(
fiber: Fiber,
nextChildren: Array<number>,
) {
if (!shouldFilterFiber(fiber)) {
nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
} else {
let child = fiber.child;
while (child !== null) {
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
let fibers: Array<Fiber> = [fiber];

while (fibers.length > 0) {
const currentFiber = fibers.shift();

if (!shouldFilterFiber(currentFiber)) {
nextChildren.push(getFiberID(getPrimaryFiber(currentFiber)));
} else {
let child = currentFiber.child;
let childInsertionIndex = 0;
while (child !== null) {
fibers.splice(childInsertionIndex, 0, child);
childInsertionIndex++;
child = child.sibling;
}
}
}
}

// Returns whether closest unfiltered fiber parent needs to reset its child list.
//
// TODO Refactor this method to be iterative as well
function updateFiberRecursively(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method needs to be migrated too, but it's not going to be as straight forward as the other two since it depends on its own recursive return value.

nextFiber: Fiber,
prevFiber: Fiber,
Expand Down