From 25b18d31c8b7ffd256416672dc21c50dd22ac3dc Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 27 Oct 2020 14:02:19 -0500 Subject: [PATCH] Traverse commit phase effects iteratively (#20094) * Move traversal logic to ReactFiberCommitWork The current traversal logic is spread between ReactFiberWorkLoop and ReactFiberCommitWork, and it's a bit awkward, especially when refactoring. Idk the ideal module structure, so for now I'd rather keep it all in one file. * Traverse commit phase effects iteratively We suspect that using the JS stack to traverse through the tree in the commit phase is slower than traversing iteratively. I've kept the recursive implementation behind a flag, both so we have the option to run an experiment comparing the two, and so we can revert it easily later if needed. --- .../src/ReactFiberCommitWork.new.js | 1328 +++++++++++++++-- .../src/ReactFiberWorkLoop.new.js | 510 +------ packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 2 + .../forks/ReactFeatureFlags.native-oss.js | 2 + .../forks/ReactFeatureFlags.test-renderer.js | 2 + .../ReactFeatureFlags.test-renderer.native.js | 2 + .../ReactFeatureFlags.test-renderer.www.js | 2 + .../shared/forks/ReactFeatureFlags.testing.js | 2 + .../forks/ReactFeatureFlags.testing.www.js | 2 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 11 files changed, 1244 insertions(+), 612 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 3664803f20..2997e809a4 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -24,6 +24,7 @@ import type {Wakeable} from 'shared/ReactTypes'; import type {ReactPriorityLevel} from './ReactInternalTypes'; import type {OffscreenState} from './ReactFiberOffscreenComponent'; import type {HookFlags} from './ReactHookEffectTags'; +import type {Flags} from './ReactFiberFlags'; import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing'; import { @@ -35,6 +36,7 @@ import { enableSuspenseCallback, enableScopeAPI, enableDoubleInvokingEffects, + enableRecursiveCommitTraversal, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -67,10 +69,21 @@ import { ContentReset, Placement, Snapshot, + Visibility, Update, Callback, - LayoutMask, Ref, + PlacementAndUpdate, + Hydrating, + HydratingAndUpdate, + Passive, + PassiveStatic, + BeforeMutationMask, + MutationMask, + LayoutMask, + PassiveMask, + MountLayoutDev, + MountPassiveDev, } from './ReactFiberFlags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; @@ -88,13 +101,20 @@ import { recordPassiveEffectDuration, startPassiveEffectTimer, } from './ReactProfilerTimer.new'; -import {ProfileMode} from './ReactTypeOfMode'; +import { + NoMode, + BlockingMode, + ConcurrentMode, + ProfileMode, +} from './ReactTypeOfMode'; import {commitUpdateQueue} from './ReactUpdateQueue.new'; import { getPublicInstance, supportsMutation, supportsPersistence, supportsHydration, + prepareForCommit, + beforeActiveInstanceBlur, commitMount, commitUpdate, resetTextContent, @@ -132,6 +152,9 @@ import { Passive as HookPassive, } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; +import {doesFiberContain} from './ReactFiberTreeReflection'; + +let nextEffect: Fiber | null = null; // Used to avoid traversing the return path to find the nearest Profiler ancestor during commit. let nearestProfilerOnStack: Fiber | null = null; @@ -256,98 +279,6 @@ export function safelyCallDestroy( } } -function commitBeforeMutationLifeCycles( - current: Fiber | null, - finishedWork: Fiber, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - return; - } - case ClassComponent: { - if (finishedWork.flags & Snapshot) { - if (current !== null) { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - const instance = finishedWork.stateNode; - // We could update instance props and state here, - // but instead we rely on them being set during last render. - // TODO: revisit this when we implement resuming. - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.props`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'getSnapshotBeforeUpdate. ' + - 'This might either be because of a bug in React, or because ' + - 'a component reassigns its own `this.state`. ' + - 'Please file an issue.', - getComponentName(finishedWork.type) || 'instance', - ); - } - } - } - const snapshot = instance.getSnapshotBeforeUpdate( - finishedWork.elementType === finishedWork.type - ? prevProps - : resolveDefaultProps(finishedWork.type, prevProps), - prevState, - ); - if (__DEV__) { - const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); - if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) { - didWarnSet.add(finishedWork.type); - console.error( - '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + - 'must be returned. You have returned undefined.', - getComponentName(finishedWork.type), - ); - } - } - instance.__reactInternalSnapshotBeforeUpdate = snapshot; - } - } - return; - } - case HostRoot: { - if (supportsMutation) { - if (finishedWork.flags & Snapshot) { - const root = finishedWork.stateNode; - clearContainer(root.containerInfo); - } - } - return; - } - case HostComponent: - case HostText: - case HostPortal: - case IncompleteClassComponent: - // Nothing to do for these component types - return; - } - invariant( - false, - 'This unit of work tag should not have side-effects. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); -} - function commitHookEffectListUnmount( flags: HookFlags, finishedWork: Fiber, @@ -462,6 +393,530 @@ function commitProfilerPassiveEffect( } } +let focusedInstanceHandle: null | Fiber = null; +let shouldFireAfterActiveInstanceBlur: boolean = false; + +export function commitBeforeMutationEffects( + root: FiberRoot, + firstChild: Fiber, +) { + focusedInstanceHandle = prepareForCommit(root.containerInfo); + + if (enableRecursiveCommitTraversal) { + recursivelyCommitBeforeMutationEffects(firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitBeforeMutationEffects_begin(); + } + + // We no longer need to track the active instance fiber + const shouldFire = shouldFireAfterActiveInstanceBlur; + shouldFireAfterActiveInstanceBlur = false; + focusedInstanceHandle = null; + + return shouldFire; +} + +function recursivelyCommitBeforeMutationEffects(firstChild: Fiber) { + let fiber = firstChild; + while (fiber !== null) { + // TODO: Should wrap this in flags check, too, as optimization + if (fiber.deletions !== null) { + commitBeforeMutationEffectsDeletions(fiber.deletions); + } + + const child = fiber.child; + if (fiber.subtreeFlags & BeforeMutationMask && child !== null) { + recursivelyCommitBeforeMutationEffects(child); + } + + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitBeforeMutationEffectsOnFiber, + null, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitBeforeMutationEffectsOnFiber(fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + fiber = fiber.sibling; + } +} + +function iterativelyCommitBeforeMutationEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + commitBeforeMutationEffectsDeletions(deletions); + } + + const child = fiber.child; + if ( + (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && + child !== null + ) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitBeforeMutationEffects_complete(); + } + } +} + +function iterativelyCommitBeforeMutationEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitBeforeMutationEffectsOnFiber, + null, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitBeforeMutationEffectsOnFiber(fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) { + const current = finishedWork.alternate; + const flags = finishedWork.flags; + + if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { + // Check to see if the focused element was inside of a hidden (Suspense) subtree. + if ( + // TODO: Can optimize this further with separate Hide and Show flags. We + // only care about Hide here. + (flags & Visibility) !== NoFlags && + finishedWork.tag === SuspenseComponent && + isSuspenseBoundaryBeingHidden(current, finishedWork) && + doesFiberContain(finishedWork, focusedInstanceHandle) + ) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(finishedWork); + } + } + + if ((flags & Snapshot) !== NoFlags) { + setCurrentDebugFiberInDEV(finishedWork); + switch (finishedWork.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + break; + } + case ClassComponent: { + if (finishedWork.flags & Snapshot) { + if (current !== null) { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + const instance = finishedWork.stateNode; + // We could update instance props and state here, + // but instead we rely on them being set during last render. + // TODO: revisit this when we implement resuming. + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.props`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'getSnapshotBeforeUpdate. ' + + 'This might either be because of a bug in React, or because ' + + 'a component reassigns its own `this.state`. ' + + 'Please file an issue.', + getComponentName(finishedWork.type) || 'instance', + ); + } + } + } + const snapshot = instance.getSnapshotBeforeUpdate( + finishedWork.elementType === finishedWork.type + ? prevProps + : resolveDefaultProps(finishedWork.type, prevProps), + prevState, + ); + if (__DEV__) { + const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set); + if ( + snapshot === undefined && + !didWarnSet.has(finishedWork.type) + ) { + didWarnSet.add(finishedWork.type); + console.error( + '%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' + + 'must be returned. You have returned undefined.', + getComponentName(finishedWork.type), + ); + } + } + instance.__reactInternalSnapshotBeforeUpdate = snapshot; + } + } + break; + } + case HostRoot: { + if (supportsMutation) { + if (finishedWork.flags & Snapshot) { + const root = finishedWork.stateNode; + clearContainer(root.containerInfo); + } + } + break; + } + case HostComponent: + case HostText: + case HostPortal: + case IncompleteClassComponent: + // Nothing to do for these component types + break; + default: + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + resetCurrentDebugFiberInDEV(); + } +} + +function commitBeforeMutationEffectsDeletions(deletions: Array) { + for (let i = 0; i < deletions.length; i++) { + const fiber = deletions[i]; + + // TODO (effects) It would be nice to avoid calling doesFiberContain() + // Maybe we can repurpose one of the subtreeFlags positions for this instead? + // Use it to store which part of the tree the focused instance is in? + // This assumes we can safely determine that instance during the "render" phase. + + if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) { + shouldFireAfterActiveInstanceBlur = true; + beforeActiveInstanceBlur(fiber); + } + } +} + +export function commitMutationEffects( + firstChild: Fiber, + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + if (enableRecursiveCommitTraversal) { + recursivelyCommitMutationEffects(firstChild, root, renderPriorityLevel); + } else { + nextEffect = firstChild; + iterativelyCommitMutationEffects_begin(root, renderPriorityLevel); + } +} + +function recursivelyCommitMutationEffects( + firstChild: Fiber, + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + let fiber = firstChild; + while (fiber !== null) { + const deletions = fiber.deletions; + if (deletions !== null) { + commitMutationEffectsDeletions( + deletions, + fiber, + root, + renderPriorityLevel, + ); + } + + if (fiber.child !== null) { + const mutationFlags = fiber.subtreeFlags & MutationMask; + if (mutationFlags !== NoFlags) { + recursivelyCommitMutationEffects( + fiber.child, + root, + renderPriorityLevel, + ); + } + } + + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitMutationEffectsOnFiber, + null, + fiber, + root, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + fiber = fiber.sibling; + } +} + +function iterativelyCommitMutationEffects_begin( + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + commitMutationEffectsDeletions( + deletions, + fiber, + root, + renderPriorityLevel, + ); + } + + const child = fiber.child; + if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitMutationEffects_complete(root, renderPriorityLevel); + } + } +} + +function iterativelyCommitMutationEffects_complete( + root: FiberRoot, + renderPriorityLevel: ReactPriorityLevel, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitMutationEffectsOnFiber, + null, + fiber, + root, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function commitMutationEffectsOnFiber( + fiber: Fiber, + root: FiberRoot, + renderPriorityLevel, +) { + const flags = fiber.flags; + if (flags & ContentReset) { + commitResetTextContent(fiber); + } + + if (flags & Ref) { + const current = fiber.alternate; + if (current !== null) { + commitDetachRef(current); + } + if (enableScopeAPI) { + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. + if (fiber.tag === ScopeComponent) { + commitAttachRef(fiber); + } + } + } + + // The following switch statement is only concerned about placement, + // updates, and deletions. To avoid needing to add a case for every possible + // bitmap value, we remove the secondary effects from the effect tag and + // switch on that value. + const primaryFlags = flags & (Placement | Update | Hydrating); + switch (primaryFlags) { + case Placement: { + commitPlacement(fiber); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + // TODO: findDOMNode doesn't rely on this any more but isMounted does + // and isMounted is deprecated anyway so we should be able to kill this. + fiber.flags &= ~Placement; + break; + } + case PlacementAndUpdate: { + // Placement + commitPlacement(fiber); + // Clear the "placement" from effect tag so that we know that this is + // inserted, before any life-cycles like componentDidMount gets called. + fiber.flags &= ~Placement; + + // Update + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + case Hydrating: { + fiber.flags &= ~Hydrating; + break; + } + case HydratingAndUpdate: { + fiber.flags &= ~Hydrating; + + // Update + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + case Update: { + const current = fiber.alternate; + commitWork(current, fiber); + break; + } + } +} + +function commitMutationEffectsDeletions( + deletions: Array, + nearestMountedAncestor: Fiber, + root: FiberRoot, + renderPriorityLevel, +) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + if (__DEV__) { + invokeGuardedCallback( + null, + commitDeletion, + null, + root, + childToDelete, + nearestMountedAncestor, + renderPriorityLevel, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); + } + } else { + try { + commitDeletion( + root, + childToDelete, + nearestMountedAncestor, + renderPriorityLevel, + ); + } catch (error) { + captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); + } + } + } +} + +export function commitLayoutEffects( + finishedWork: Fiber, + finishedRoot: FiberRoot, +) { + if (enableRecursiveCommitTraversal) { + if (__DEV__) { + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + finishedWork, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, null, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + recursivelyCommitLayoutEffects(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, null, error); + } + } + } else { + nextEffect = finishedWork; + iterativelyCommitLayoutEffects_begin(finishedWork, finishedRoot); + } +} + function recursivelyCommitLayoutEffects( finishedWork: Fiber, finishedRoot: FiberRoot, @@ -676,6 +1131,217 @@ function recursivelyCommitLayoutEffects( } } +function iterativelyCommitLayoutEffects_begin( + subtreeRoot: Fiber, + finishedRoot: FiberRoot, +) { + while (nextEffect !== null) { + const finishedWork: Fiber = nextEffect; + const firstChild = finishedWork.child; + + if ( + (finishedWork.subtreeFlags & LayoutMask) !== NoFlags && + firstChild !== null + ) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.tag === Profiler + ) { + const prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = finishedWork; + + let child = firstChild; + while (child !== null) { + nextEffect = child; + iterativelyCommitLayoutEffects_begin(child, finishedRoot); + child = child.sibling; + } + nextEffect = finishedWork; + + if ((finishedWork.flags & LayoutMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + commitLayoutEffectsForProfiler, + null, + finishedWork, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + + // Propagate layout effect durations to the next nearest Profiler ancestor. + // Do not reset these values until the next render so DevTools has a chance to read them first. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.effectDuration += + finishedWork.stateNode.effectDuration; + } + nearestProfilerOnStack = prevProfilerOnStack; + + if (finishedWork === subtreeRoot) { + nextEffect = null; + return; + } + const sibling = finishedWork.sibling; + if (sibling !== null) { + sibling.return = finishedWork.return; + nextEffect = sibling; + } else { + nextEffect = finishedWork.return; + iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot); + } + } else { + firstChild.return = finishedWork; + nextEffect = firstChild; + } + } else { + iterativelyCommitLayoutEffects_complete(subtreeRoot, finishedRoot); + } + } +} + +function iterativelyCommitLayoutEffects_complete( + subtreeRoot: Fiber, + finishedRoot: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + + if ((fiber.flags & LayoutMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitLayoutEffectsOnFiber, + null, + finishedRoot, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitLayoutEffectsOnFiber(finishedRoot, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = nextEffect.return; + } +} + +function commitLayoutEffectsOnFiber( + finishedRoot: FiberRoot, + finishedWork: Fiber, +) { + const tag = finishedWork.tag; + const flags = finishedWork.flags; + if ((flags & (Update | Callback)) !== NoFlags) { + switch (tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + } + break; + } + case ClassComponent: { + // NOTE: Layout effect durations are measured within this function. + commitLayoutEffectsForClassComponent(finishedWork); + break; + } + case HostRoot: { + commitLayoutEffectsForHostRoot(finishedWork); + break; + } + case HostComponent: { + commitLayoutEffectsForHostComponent(finishedWork); + break; + } + case Profiler: { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + break; + } + case SuspenseComponent: { + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); + break; + } + case FundamentalComponent: + case HostPortal: + case HostText: + case IncompleteClassComponent: + case LegacyHiddenComponent: + case OffscreenComponent: + case ScopeComponent: + case SuspenseListComponent: { + // We have no life-cycles associated with these component types. + break; + } + default: { + invariant( + false, + 'This unit of work tag should not have side-effects. This error is ' + + 'likely caused by a bug in React. Please file an issue.', + ); + } + } + } + + if (enableScopeAPI) { + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. + if (flags & Ref && tag !== ScopeComponent) { + commitAttachRef(finishedWork); + } + } else { + if (flags & Ref) { + commitAttachRef(finishedWork); + } + } +} + function commitLayoutEffectsForProfiler( finishedWork: Fiber, finishedRoot: FiberRoot, @@ -972,6 +1638,401 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) { } } +export function commitPassiveMountEffects( + root: FiberRoot, + firstChild: Fiber, +): void { + if (enableRecursiveCommitTraversal) { + recursivelyCommitPassiveMountEffects(root, firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitPassiveMountEffects_begin(firstChild, root); + } +} + +function recursivelyCommitPassiveMountEffects( + root: FiberRoot, + firstChild: Fiber, +): void { + let fiber = firstChild; + while (fiber !== null) { + let prevProfilerOnStack = null; + if (enableProfilerTimer && enableProfilerCommitHooks) { + if (fiber.tag === Profiler) { + prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = fiber; + } + } + + const primarySubtreeFlags = fiber.subtreeFlags & PassiveMask; + + if (fiber.child !== null && primarySubtreeFlags !== NoFlags) { + recursivelyCommitPassiveMountEffects(root, fiber.child); + } + + if ((fiber.flags & Passive) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitPassiveMountOnFiber, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveMountOnFiber(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (enableProfilerTimer && enableProfilerCommitHooks) { + if (fiber.tag === Profiler) { + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.passiveEffectDuration += + fiber.stateNode.passiveEffectDuration; + } + + nearestProfilerOnStack = prevProfilerOnStack; + } + } + + fiber = fiber.sibling; + } +} + +function iterativelyCommitPassiveMountEffects_begin( + subtreeRoot: Fiber, + root: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const firstChild = fiber.child; + if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.tag === Profiler + ) { + const prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = fiber; + + let child = firstChild; + while (child !== null) { + nextEffect = child; + iterativelyCommitPassiveMountEffects_begin(child, root); + child = child.sibling; + } + nextEffect = fiber; + + if ((fiber.flags & PassiveMask) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitProfilerPassiveEffect, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitProfilerPassiveEffect(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + // Bubble times to the next nearest ancestor Profiler. + // After we process that Profiler, we'll bubble further up. + if (prevProfilerOnStack !== null) { + prevProfilerOnStack.stateNode.passiveEffectDuration += + fiber.stateNode.passiveEffectDuration; + } + + nearestProfilerOnStack = prevProfilerOnStack; + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + } else { + nextEffect = fiber.return; + iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root); + } + } else { + firstChild.return = fiber; + nextEffect = firstChild; + } + } else { + iterativelyCommitPassiveMountEffects_complete(subtreeRoot, root); + } + } +} + +function iterativelyCommitPassiveMountEffects_complete( + subtreeRoot: Fiber, + root: FiberRoot, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & Passive) !== NoFlags) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + invokeGuardedCallback( + null, + commitPassiveMountOnFiber, + null, + root, + fiber, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(fiber, fiber.return, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + commitPassiveMountOnFiber(root, fiber); + } catch (error) { + captureCommitPhaseError(fiber, fiber.return, error); + } + } + } + + if (fiber === subtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +export function commitPassiveUnmountEffects(firstChild: Fiber): void { + if (enableRecursiveCommitTraversal) { + recursivelyCommitPassiveUnmountEffects(firstChild); + } else { + nextEffect = firstChild; + iterativelyCommitPassiveUnmountEffects_begin(); + } +} + +function recursivelyCommitPassiveUnmountEffects(firstChild: Fiber): void { + let fiber = firstChild; + while (fiber !== null) { + const deletions = fiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const fiberToDelete = deletions[i]; + recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + fiberToDelete, + fiber, + ); + + // Now that passive effects have been processed, it's safe to detach lingering pointers. + detachFiberAfterEffects(fiberToDelete); + } + } + + const child = fiber.child; + if (child !== null) { + // If any children have passive effects then traverse the subtree. + // Note that this requires checking subtreeFlags of the current Fiber, + // rather than the subtreeFlags/effectsTag of the first child, + // since that would not cover passive effects in siblings. + const passiveFlags = fiber.subtreeFlags & PassiveMask; + if (passiveFlags !== NoFlags) { + recursivelyCommitPassiveUnmountEffects(child); + } + } + + const primaryFlags = fiber.flags & Passive; + if (primaryFlags !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + fiber = fiber.sibling; + } +} + +function iterativelyCommitPassiveUnmountEffects_begin() { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + + // TODO: Should wrap this in flags check, too, as optimization + const deletions = fiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const fiberToDelete = deletions[i]; + nextEffect = fiberToDelete; + iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin( + fiberToDelete, + fiber, + ); + + // Now that passive effects have been processed, it's safe to detach lingering pointers. + detachFiberAfterEffects(fiberToDelete); + } + nextEffect = fiber; + } + + if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitPassiveUnmountEffects_complete(); + } + } +} + +function iterativelyCommitPassiveUnmountEffects_complete() { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & Passive) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountOnFiber(fiber); + resetCurrentDebugFiberInDEV(); + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + fiberToDelete: Fiber, + nearestMountedAncestor: Fiber, +): void { + if ((fiberToDelete.subtreeFlags & PassiveStatic) !== NoFlags) { + // If any children have passive effects then traverse the subtree. + // Note that this requires checking subtreeFlags of the current Fiber, + // rather than the subtreeFlags/effectsTag of the first child, + // since that would not cover passive effects in siblings. + let child = fiberToDelete.child; + while (child !== null) { + recursivelyCommitPassiveUnmountEffectsInsideOfDeletedTree( + child, + nearestMountedAncestor, + ); + child = child.sibling; + } + } + + if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) { + setCurrentDebugFiberInDEV(fiberToDelete); + commitPassiveUnmountInsideDeletedTreeOnFiber( + fiberToDelete, + nearestMountedAncestor, + ); + resetCurrentDebugFiberInDEV(); + } +} + +function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_begin( + deletedSubtreeRoot: Fiber, + nearestMountedAncestor: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + const child = fiber.child; + if ((fiber.subtreeFlags & PassiveStatic) !== NoFlags && child !== null) { + child.return = fiber; + nextEffect = child; + } else { + iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot, + nearestMountedAncestor, + ); + } + } +} + +function iterativelyCommitPassiveUnmountEffectsInsideOfDeletedTree_complete( + deletedSubtreeRoot: Fiber, + nearestMountedAncestor: Fiber, +) { + while (nextEffect !== null) { + const fiber = nextEffect; + if ((fiber.flags & PassiveStatic) !== NoFlags) { + setCurrentDebugFiberInDEV(fiber); + commitPassiveUnmountInsideDeletedTreeOnFiber( + fiber, + nearestMountedAncestor, + ); + resetCurrentDebugFiberInDEV(); + } + + if (fiber === deletedSubtreeRoot) { + nextEffect = null; + return; + } + + const sibling = fiber.sibling; + if (sibling !== null) { + sibling.return = fiber.return; + nextEffect = sibling; + return; + } + + nextEffect = fiber.return; + } +} + +function detachFiberAfterEffects(fiber: Fiber): void { + // Null out fields to improve GC for references that may be lingering (e.g. DevTools). + // Note that we already cleared the return pointer in detachFiberMutation(). + fiber.child = null; + fiber.deletions = null; + fiber.dependencies = null; + fiber.memoizedProps = null; + fiber.memoizedState = null; + fiber.pendingProps = null; + fiber.sibling = null; + fiber.stateNode = null; + fiber.updateQueue = null; + + if (__DEV__) { + fiber._debugOwner = null; + } +} + function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref; if (ref !== null) { @@ -1950,7 +3011,7 @@ function attachSuspenseRetryListeners(finishedWork: Fiber) { // This function detects when a Suspense boundary goes from visible to hidden. // It returns false if the boundary is already hidden. // TODO: Use an effect tag. -export function isSuspenseBoundaryBeingHidden( +function isSuspenseBoundaryBeingHidden( current: Fiber | null, finishedWork: Fiber, ): boolean { @@ -1971,7 +3032,7 @@ function commitResetTextContent(current: Fiber): void { resetTextContent(current.stateNode); } -function commitPassiveUnmount(finishedWork: Fiber): void { +function commitPassiveUnmountOnFiber(finishedWork: Fiber): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -2001,7 +3062,7 @@ function commitPassiveUnmount(finishedWork: Fiber): void { } } -function commitPassiveUnmountInsideDeletedTree( +function commitPassiveUnmountInsideDeletedTreeOnFiber( current: Fiber, nearestMountedAncestor: Fiber | null, ): void { @@ -2034,7 +3095,7 @@ function commitPassiveUnmountInsideDeletedTree( } } -function commitPassiveMount( +function commitPassiveMountOnFiber( finishedRoot: FiberRoot, finishedWork: Fiber, ): void { @@ -2188,20 +3249,57 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void { } } -export { - commitBeforeMutationLifeCycles, - commitResetTextContent, - commitPlacement, - commitDeletion, - commitWork, - commitAttachRef, - commitDetachRef, - commitPassiveUnmount, - commitPassiveUnmountInsideDeletedTree, - commitPassiveMount, - invokeLayoutEffectMountInDEV, - invokeLayoutEffectUnmountInDEV, - invokePassiveEffectMountInDEV, - invokePassiveEffectUnmountInDEV, - recursivelyCommitLayoutEffects, -}; +// TODO: Convert this to iteration instead of recursion, too. Leaving this for +// a follow up because the flag is off. +export function commitDoubleInvokeEffectsInDEV( + fiber: Fiber, + hasPassiveEffects: boolean, +) { + if (__DEV__ && enableDoubleInvokingEffects) { + // Never double-invoke effects for legacy roots. + if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) { + return; + } + + setCurrentDebugFiberInDEV(fiber); + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV); + if (hasPassiveEffects) { + invokeEffectsInDev( + fiber, + MountPassiveDev, + invokePassiveEffectUnmountInDEV, + ); + } + + invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV); + if (hasPassiveEffects) { + invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV); + } + resetCurrentDebugFiberInDEV(); + } +} + +function invokeEffectsInDev( + firstChild: Fiber, + fiberFlags: Flags, + invokeEffectFn: (fiber: Fiber) => void, +): void { + if (__DEV__ && enableDoubleInvokingEffects) { + // We don't need to re-check for legacy roots here. + // This function will not be called within legacy roots. + let fiber = firstChild; + while (fiber !== null) { + if (fiber.child !== null) { + const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags; + if (primarySubtreeFlag !== NoFlags) { + invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn); + } + } + + if ((fiber.flags & fiberFlags) !== NoFlags) { + invokeEffectFn(fiber); + } + fiber = fiber.sibling; + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 33d8398fad..ee2df3564c 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -15,21 +15,18 @@ import type {Interaction} from 'scheduler/src/Tracing'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {StackCursor} from './ReactFiberStack.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; -import type {Flags} from './ReactFiberFlags'; import { warnAboutDeprecatedLifecycles, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, - enableProfilerCommitHooks, enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, decoupleUpdatePriorityFromScheduler, enableDebugTracing, enableSchedulingProfiler, - enableScopeAPI, skipUnmountedBoundaries, enableDoubleInvokingEffects, } from 'shared/ReactFeatureFlags'; @@ -83,13 +80,11 @@ import * as Scheduler from 'scheduler'; import {__interactionsRef, __subscriberRef} from 'scheduler/tracing'; import { - prepareForCommit, resetAfterCommit, scheduleTimeout, cancelTimeout, noTimeout, warnsIfNotActing, - beforeActiveInstanceBlur, afterActiveInstanceBlur, clearContainer, } from './ReactFiberHostConfig'; @@ -116,31 +111,19 @@ import { MemoComponent, SimpleMemoComponent, Block, - ScopeComponent, - Profiler, } from './ReactWorkTags'; import {LegacyRoot} from './ReactRootTags'; import { NoFlags, Placement, - Update, - PlacementAndUpdate, - Ref, - ContentReset, - Snapshot, - Passive, PassiveStatic, Incomplete, HostEffectMask, Hydrating, - HydratingAndUpdate, - Visibility, BeforeMutationMask, MutationMask, LayoutMask, PassiveMask, - MountPassiveDev, - MountLayoutDev, } from './ReactFiberFlags'; import { NoLanePriority, @@ -191,22 +174,12 @@ import { createClassErrorUpdate, } from './ReactFiberThrow.new'; import { - commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, - commitPlacement, - commitWork, - commitDeletion, - commitPassiveUnmount as commitPassiveUnmountOnFiber, - commitPassiveUnmountInsideDeletedTree as commitPassiveUnmountInsideDeletedTreeOnFiber, - commitPassiveMount as commitPassiveMountOnFiber, - commitDetachRef, - commitAttachRef, - commitResetTextContent, - isSuspenseBoundaryBeingHidden, - invokeLayoutEffectMountInDEV, - invokePassiveEffectMountInDEV, - invokeLayoutEffectUnmountInDEV, - invokePassiveEffectUnmountInDEV, - recursivelyCommitLayoutEffects, + commitBeforeMutationEffects, + commitMutationEffects, + commitLayoutEffects, + commitPassiveMountEffects, + commitPassiveUnmountEffects, + commitDoubleInvokeEffectsInDEV, } from './ReactFiberCommitWork.new'; import {enqueueUpdate} from './ReactUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; @@ -247,7 +220,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors'; // Used by `act` import enqueueTask from 'shared/enqueueTask'; -import {doesFiberContain} from './ReactFiberTreeReflection'; const ceil = Math.ceil; @@ -327,9 +299,6 @@ let workInProgressRootRenderTargetTime: number = Infinity; // suspense heuristics and opt out of rendering more content. const RENDER_TIMEOUT_MS = 500; -// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit. -let nearestProfilerOnStack: Fiber | null = null; - function resetRenderTimer() { workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS; } @@ -374,9 +343,6 @@ let currentEventPendingLanes: Lanes = NoLanes; // We warn about state updates for unmounted components differently in this case. let isFlushingPassiveEffects = false; -let focusedInstanceHandle: null | Fiber = null; -let shouldFireAfterActiveInstanceBlur: boolean = false; - export function getWorkInProgressRoot(): FiberRoot | null { return workInProgressRoot; } @@ -1916,13 +1882,10 @@ function commitRootImpl(root, renderPriorityLevel) { // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. - focusedInstanceHandle = prepareForCommit(root.containerInfo); - shouldFireAfterActiveInstanceBlur = false; - - commitBeforeMutationEffects(finishedWork); - - // We no longer need to track the active instance fiber - focusedInstanceHandle = null; + const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects( + root, + finishedWork, + ); if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this @@ -1957,27 +1920,7 @@ function commitRootImpl(root, renderPriorityLevel) { markLayoutEffectsStarted(lanes); } - if (__DEV__) { - setCurrentDebugFiberInDEV(finishedWork); - invokeGuardedCallback( - null, - recursivelyCommitLayoutEffects, - null, - finishedWork, - root, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - recursivelyCommitLayoutEffects(finishedWork, root); - } catch (error) { - captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error); - } - } + commitLayoutEffects(finishedWork, root); if (__DEV__) { if (enableDebugTracing) { @@ -2117,238 +2060,6 @@ function commitRootImpl(root, renderPriorityLevel) { return null; } -function commitBeforeMutationEffects(firstChild: Fiber) { - let fiber = firstChild; - while (fiber !== null) { - if (fiber.deletions !== null) { - commitBeforeMutationEffectsDeletions(fiber.deletions); - } - - if (fiber.child !== null) { - const primarySubtreeFlags = fiber.subtreeFlags & BeforeMutationMask; - if (primarySubtreeFlags !== NoFlags) { - commitBeforeMutationEffects(fiber.child); - } - } - - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - invokeGuardedCallback(null, commitBeforeMutationEffectsImpl, null, fiber); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitBeforeMutationEffectsImpl(fiber); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - } - fiber = fiber.sibling; - } -} - -function commitBeforeMutationEffectsImpl(fiber: Fiber) { - const current = fiber.alternate; - const flags = fiber.flags; - - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { - // Check to see if the focused element was inside of a hidden (Suspense) subtree. - if ( - // TODO: Can optimize this further with separate Hide and Show flags. We - // only care about Hide here. - (flags & Visibility) !== NoFlags && - fiber.tag === SuspenseComponent && - isSuspenseBoundaryBeingHidden(current, fiber) && - doesFiberContain(fiber, focusedInstanceHandle) - ) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(fiber); - } - } - - if ((flags & Snapshot) !== NoFlags) { - setCurrentDebugFiberInDEV(fiber); - commitBeforeMutationEffectOnFiber(current, fiber); - resetCurrentDebugFiberInDEV(); - } -} - -function commitBeforeMutationEffectsDeletions(deletions: Array) { - for (let i = 0; i < deletions.length; i++) { - const fiber = deletions[i]; - - // TODO (effects) It would be nice to avoid calling doesFiberContain() - // Maybe we can repurpose one of the subtreeFlags positions for this instead? - // Use it to store which part of the tree the focused instance is in? - // This assumes we can safely determine that instance during the "render" phase. - - if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) { - shouldFireAfterActiveInstanceBlur = true; - beforeActiveInstanceBlur(fiber); - } - } -} - -function commitMutationEffects( - firstChild: Fiber, - root: FiberRoot, - renderPriorityLevel: ReactPriorityLevel, -) { - let fiber = firstChild; - while (fiber !== null) { - const deletions = fiber.deletions; - if (deletions !== null) { - commitMutationEffectsDeletions( - deletions, - fiber, - root, - renderPriorityLevel, - ); - } - - if (fiber.child !== null) { - const mutationFlags = fiber.subtreeFlags & MutationMask; - if (mutationFlags !== NoFlags) { - commitMutationEffects(fiber.child, root, renderPriorityLevel); - } - } - - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - invokeGuardedCallback( - null, - commitMutationEffectsImpl, - null, - fiber, - root, - renderPriorityLevel, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitMutationEffectsImpl(fiber, root, renderPriorityLevel); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - } - fiber = fiber.sibling; - } -} - -function commitMutationEffectsImpl( - fiber: Fiber, - root: FiberRoot, - renderPriorityLevel, -) { - const flags = fiber.flags; - if (flags & ContentReset) { - commitResetTextContent(fiber); - } - - if (flags & Ref) { - const current = fiber.alternate; - if (current !== null) { - commitDetachRef(current); - } - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. - if (fiber.tag === ScopeComponent) { - commitAttachRef(fiber); - } - } - } - - // The following switch statement is only concerned about placement, - // updates, and deletions. To avoid needing to add a case for every possible - // bitmap value, we remove the secondary effects from the effect tag and - // switch on that value. - const primaryFlags = flags & (Placement | Update | Hydrating); - switch (primaryFlags) { - case Placement: { - commitPlacement(fiber); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - // TODO: findDOMNode doesn't rely on this any more but isMounted does - // and isMounted is deprecated anyway so we should be able to kill this. - fiber.flags &= ~Placement; - break; - } - case PlacementAndUpdate: { - // Placement - commitPlacement(fiber); - // Clear the "placement" from effect tag so that we know that this is - // inserted, before any life-cycles like componentDidMount gets called. - fiber.flags &= ~Placement; - - // Update - const current = fiber.alternate; - commitWork(current, fiber); - break; - } - case Hydrating: { - fiber.flags &= ~Hydrating; - break; - } - case HydratingAndUpdate: { - fiber.flags &= ~Hydrating; - - // Update - const current = fiber.alternate; - commitWork(current, fiber); - break; - } - case Update: { - const current = fiber.alternate; - commitWork(current, fiber); - break; - } - } -} - -function commitMutationEffectsDeletions( - deletions: Array, - nearestMountedAncestor: Fiber, - root: FiberRoot, - renderPriorityLevel, -) { - for (let i = 0; i < deletions.length; i++) { - const childToDelete = deletions[i]; - if (__DEV__) { - invokeGuardedCallback( - null, - commitDeletion, - null, - root, - childToDelete, - nearestMountedAncestor, - renderPriorityLevel, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); - } - } else { - try { - commitDeletion( - root, - childToDelete, - nearestMountedAncestor, - renderPriorityLevel, - ); - } catch (error) { - captureCommitPhaseError(childToDelete, nearestMountedAncestor, error); - } - } - } -} - export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) { @@ -2374,130 +2085,6 @@ export function flushPassiveEffects(): boolean { return false; } -function flushPassiveMountEffects(root, firstChild: Fiber): void { - let fiber = firstChild; - while (fiber !== null) { - let prevProfilerOnStack = null; - if (enableProfilerTimer && enableProfilerCommitHooks) { - if (fiber.tag === Profiler) { - prevProfilerOnStack = nearestProfilerOnStack; - nearestProfilerOnStack = fiber; - } - } - - const primarySubtreeFlags = fiber.subtreeFlags & PassiveMask; - - if (fiber.child !== null && primarySubtreeFlags !== NoFlags) { - flushPassiveMountEffects(root, fiber.child); - } - - if ((fiber.flags & Passive) !== NoFlags) { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - invokeGuardedCallback( - null, - commitPassiveMountOnFiber, - null, - root, - fiber, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitPassiveMountOnFiber(root, fiber); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - } - } - - if (enableProfilerTimer && enableProfilerCommitHooks) { - if (fiber.tag === Profiler) { - // Bubble times to the next nearest ancestor Profiler. - // After we process that Profiler, we'll bubble further up. - if (prevProfilerOnStack !== null) { - prevProfilerOnStack.stateNode.passiveEffectDuration += - fiber.stateNode.passiveEffectDuration; - } - - nearestProfilerOnStack = prevProfilerOnStack; - } - } - - fiber = fiber.sibling; - } -} - -function flushPassiveUnmountEffects(firstChild: Fiber): void { - let fiber = firstChild; - while (fiber !== null) { - const deletions = fiber.deletions; - if (deletions !== null) { - for (let i = 0; i < deletions.length; i++) { - const fiberToDelete = deletions[i]; - flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete, fiber); - - // Now that passive effects have been processed, it's safe to detach lingering pointers. - detachFiberAfterEffects(fiberToDelete); - } - } - - const child = fiber.child; - if (child !== null) { - // If any children have passive effects then traverse the subtree. - // Note that this requires checking subtreeFlags of the current Fiber, - // rather than the subtreeFlags/effectsTag of the first child, - // since that would not cover passive effects in siblings. - const passiveFlags = fiber.subtreeFlags & PassiveMask; - if (passiveFlags !== NoFlags) { - flushPassiveUnmountEffects(child); - } - } - - const primaryFlags = fiber.flags & Passive; - if (primaryFlags !== NoFlags) { - setCurrentDebugFiberInDEV(fiber); - commitPassiveUnmountOnFiber(fiber); - resetCurrentDebugFiberInDEV(); - } - - fiber = fiber.sibling; - } -} - -function flushPassiveUnmountEffectsInsideOfDeletedTree( - fiberToDelete: Fiber, - nearestMountedAncestor: Fiber, -): void { - if ((fiberToDelete.subtreeFlags & PassiveStatic) !== NoFlags) { - // If any children have passive effects then traverse the subtree. - // Note that this requires checking subtreeFlags of the current Fiber, - // rather than the subtreeFlags/effectsTag of the first child, - // since that would not cover passive effects in siblings. - let child = fiberToDelete.child; - while (child !== null) { - flushPassiveUnmountEffectsInsideOfDeletedTree( - child, - nearestMountedAncestor, - ); - child = child.sibling; - } - } - - if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) { - setCurrentDebugFiberInDEV(fiberToDelete); - commitPassiveUnmountInsideDeletedTreeOnFiber( - fiberToDelete, - nearestMountedAncestor, - ); - resetCurrentDebugFiberInDEV(); - } -} - function flushPassiveEffectsImpl() { if (rootWithPendingPassiveEffects === null) { return false; @@ -2537,8 +2124,8 @@ function flushPassiveEffectsImpl() { // e.g. a destroy function in one component may unintentionally override a ref // value set by a create function in another component. // Layout effects have the same constraint. - flushPassiveUnmountEffects(root.current); - flushPassiveMountEffects(root, root.current); + commitPassiveUnmountEffects(root.current); + commitPassiveMountEffects(root, root.current); if (__DEV__) { if (enableDebugTracing) { @@ -2840,59 +2427,6 @@ function flushRenderPhaseStrictModeWarningsInDEV() { } } -function commitDoubleInvokeEffectsInDEV( - fiber: Fiber, - hasPassiveEffects: boolean, -) { - if (__DEV__ && enableDoubleInvokingEffects) { - // Never double-invoke effects for legacy roots. - if ((fiber.mode & (BlockingMode | ConcurrentMode)) === NoMode) { - return; - } - - setCurrentDebugFiberInDEV(fiber); - invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectUnmountInDEV); - if (hasPassiveEffects) { - invokeEffectsInDev( - fiber, - MountPassiveDev, - invokePassiveEffectUnmountInDEV, - ); - } - - invokeEffectsInDev(fiber, MountLayoutDev, invokeLayoutEffectMountInDEV); - if (hasPassiveEffects) { - invokeEffectsInDev(fiber, MountPassiveDev, invokePassiveEffectMountInDEV); - } - resetCurrentDebugFiberInDEV(); - } -} - -function invokeEffectsInDev( - firstChild: Fiber, - fiberFlags: Flags, - invokeEffectFn: (fiber: Fiber) => void, -): void { - if (__DEV__ && enableDoubleInvokingEffects) { - // We don't need to re-check for legacy roots here. - // This function will not be called within legacy roots. - let fiber = firstChild; - while (fiber !== null) { - if (fiber.child !== null) { - const primarySubtreeFlag = fiber.subtreeFlags & fiberFlags; - if (primarySubtreeFlag !== NoFlags) { - invokeEffectsInDev(fiber.child, fiberFlags, invokeEffectFn); - } - } - - if ((fiber.flags & fiberFlags) !== NoFlags) { - invokeEffectFn(fiber); - } - fiber = fiber.sibling; - } - } -} - let didWarnStateUpdateForNotYetMountedComponent: Set | null = null; function warnAboutUpdateOnNotYetMountedFiberInDEV(fiber) { if (__DEV__) { @@ -3675,21 +3209,3 @@ export function act(callback: () => Thenable): Thenable { }; } } - -function detachFiberAfterEffects(fiber: Fiber): void { - // Null out fields to improve GC for references that may be lingering (e.g. DevTools). - // Note that we already cleared the return pointer in detachFiberMutation(). - fiber.child = null; - fiber.deletions = null; - fiber.dependencies = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.sibling = null; - fiber.stateNode = null; - fiber.updateQueue = null; - - if (__DEV__) { - fiber._debugOwner = null; - } -} diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index dbf9f5bd28..ba92ea1715 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; + +export const enableRecursiveCommitTraversal = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0df33470e5..2009366d12 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -53,6 +53,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 77148d9be2..aa53af7e58 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 7e66eb9e75..eee1cb9886 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 90c4360c6d..f2b19e6dc2 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 4dbd671409..11989ccafd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js index c98063376f..7bb6c2bd8b 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = false; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js index 362569dfb0..23da561303 100644 --- a/packages/shared/forks/ReactFeatureFlags.testing.www.js +++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js @@ -52,6 +52,8 @@ export const enableDiscreteEventFlushingChange = true; export const enableDoubleInvokingEffects = false; export const enableUseRefAccessWarning = false; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index f5861a2e33..fec7e2beaa 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -84,6 +84,8 @@ export const enableDiscreteEventFlushingChange = true; // to the correct value. export const enableNewReconciler = __VARIANT__; +export const enableRecursiveCommitTraversal = false; + // Flow magic to verify the exports of this file match the original version. // eslint-disable-next-line no-unused-vars type Check<_X, Y: _X, X: Y = _X> = null;