From 9198a5cec0936a21a5ba194a22fcbac03eba5d1d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 29 Sep 2020 15:58:20 -0400 Subject: [PATCH] Refactor layout effect methods (#19895) Commit phase durations (layout and passive) are stored on the nearest (ancestor) Profiler and bubble up during the commit phase. This bubbling used to be implemented by traversing the return path each time we finished working on a Profiler to find the next nearest Profiler. This commit removes that traversal. Instead, we maintain a stack of nearest Profiler ancestor while recursing the tree. This stack is maintained in the work loop (since that's where the recursive functions are) and so bubbling of durations has also been moved from commit-work to the work loop. This PR also refactors the methods used to recurse and apply effects in preparation for the new Offscreen component type. --- .../src/ReactFiberCommitWork.new.js | 729 +++++++++++------- .../src/ReactFiberWorkLoop.new.js | 130 ++-- 2 files changed, 487 insertions(+), 372 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index 6d47fac3f4661..d8843eb0ca0d4 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -17,7 +17,6 @@ import type { } from './ReactFiberHostConfig'; import type {Fiber} from './ReactInternalTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {Lanes} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {UpdateQueue} from './ReactUpdateQueue.new'; import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; @@ -70,11 +69,17 @@ import { Snapshot, Update, Callback, + LayoutMask, PassiveMask, + Ref, } from './ReactFiberFlags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; - +import { + current as currentDebugFiberInDEV, + resetCurrentFiber as resetCurrentDebugFiberInDEV, + setCurrentFiber as setCurrentDebugFiberInDEV, +} from './ReactCurrentFiber'; import {onCommitUnmount} from './ReactFiberDevToolsHook.new'; import {resolveDefaultProps} from './ReactFiberLazyComponent.new'; import { @@ -130,6 +135,9 @@ import { } from './ReactHookEffectTags'; import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new'; +// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit. +let nearestProfilerOnStack: Fiber | null = null; + let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { didWarnAboutUndefinedSnapshotBeforeUpdate = new Set(); @@ -424,19 +432,6 @@ function commitProfilerPassiveEffect( ); } } - - // Bubble times to the next nearest ancestor Profiler. - // After we process that Profiler, we'll bubble further up. - // TODO: Use JS Stack instead - let parentFiber = finishedWork.return; - while (parentFiber !== null) { - if (parentFiber.tag === Profiler) { - const parentStateNode = parentFiber.stateNode; - parentStateNode.passiveEffectDuration += passiveEffectDuration; - break; - } - parentFiber = parentFiber.return; - } break; } default: @@ -445,328 +440,470 @@ function commitProfilerPassiveEffect( } } -function commitLifeCycles( - finishedRoot: FiberRoot, - current: Fiber | null, +function recursivelyCommitLayoutEffects( finishedWork: Fiber, - committedLanes: Lanes, -): void { - switch (finishedWork.tag) { - case FunctionComponent: - case ForwardRef: - case SimpleMemoComponent: - case Block: { - // At this point layout effects have already been destroyed (during mutation phase). - // This is done to prevent sibling component effects from interfering with each other, - // e.g. a destroy function in one component should never override a ref set - // by a create function in another component during the same commit. - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); - } finally { - recordLayoutEffectDuration(finishedWork); - } - } else { - commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); + finishedRoot: FiberRoot, +) { + const {flags, tag} = finishedWork; + switch (tag) { + case Profiler: { + let prevProfilerOnStack = null; + if (enableProfilerTimer && enableProfilerCommitHooks) { + prevProfilerOnStack = nearestProfilerOnStack; + nearestProfilerOnStack = finishedWork; } - if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) { - schedulePassiveEffectCallback(); - } - return; - } - case ClassComponent: { - const instance = finishedWork.stateNode; - if (finishedWork.flags & Update) { - if (current === null) { - // 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. + let child = finishedWork.child; + while (child !== null) { + const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask; + if (primarySubtreeFlags !== NoFlags) { if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidMount. ' + - '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 ' + - 'componentDidMount. ' + - '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 prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(child); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + child, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(child, finishedWork, error); } - } - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - instance.componentDidMount(); - } finally { - recordLayoutEffectDuration(finishedWork); + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); } } else { - instance.componentDidMount(); + try { + recursivelyCommitLayoutEffects(child, finishedRoot); + } catch (error) { + captureCommitPhaseError(child, finishedWork, error); + } } - } else { - const prevProps = - finishedWork.elementType === finishedWork.type - ? current.memoizedProps - : resolveDefaultProps(finishedWork.type, current.memoizedProps); - const prevState = current.memoizedState; - // 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. + } + child = child.sibling; + } + + const primaryFlags = flags & (Update | Callback); + if (primaryFlags !== NoFlags) { + if (enableProfilerTimer) { if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'componentDidUpdate. ' + - '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 ' + - 'componentDidUpdate. ' + - '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 prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(finishedWork); + invokeGuardedCallback( + null, + commitLayoutEffectsForProfiler, + null, + finishedWork, + finishedRoot, + ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(finishedWork, finishedWork.return, error); } - } - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - finishedWork.mode & ProfileMode - ) { - try { - startLayoutEffectTimer(); - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, - ); - } finally { - recordLayoutEffectDuration(finishedWork); + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); } } else { - instance.componentDidUpdate( - prevProps, - prevState, - instance.__reactInternalSnapshotBeforeUpdate, + try { + commitLayoutEffectsForProfiler(finishedWork, finishedRoot); + } catch (error) { + captureCommitPhaseError(finishedWork, finishedWork.return, error); + } + } + } + } + + if (enableProfilerTimer && enableProfilerCommitHooks) { + // 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; + } + break; + } + + // case Offscreen: { + // TODO: Fast path to invoke all nested layout effects when Offscren goes from hidden to visible. + // break; + // } + + default: { + let child = finishedWork.child; + while (child !== null) { + const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask; + if (primarySubtreeFlags !== NoFlags) { + if (__DEV__) { + const prevCurrentFiberInDEV = currentDebugFiberInDEV; + setCurrentDebugFiberInDEV(child); + invokeGuardedCallback( + null, + recursivelyCommitLayoutEffects, + null, + child, + finishedRoot, ); + if (hasCaughtError()) { + const error = clearCaughtError(); + captureCommitPhaseError(child, finishedWork, error); + } + if (prevCurrentFiberInDEV !== null) { + setCurrentDebugFiberInDEV(prevCurrentFiberInDEV); + } else { + resetCurrentDebugFiberInDEV(); + } + } else { + try { + recursivelyCommitLayoutEffects(child, finishedRoot); + } catch (error) { + captureCommitPhaseError(child, finishedWork, error); + } } } + child = child.sibling; } - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue< - *, - > | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - if (__DEV__) { - if ( - finishedWork.type === finishedWork.elementType && - !didWarnAboutReassigningProps - ) { - if (instance.props !== finishedWork.memoizedProps) { - console.error( - 'Expected %s props to match memoized props before ' + - 'processing the update queue. ' + - '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', + const primaryFlags = flags & (Update | Callback); + if (primaryFlags !== 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, ); } - if (instance.state !== finishedWork.memoizedState) { - console.error( - 'Expected %s state to match memoized state before ' + - 'processing the update queue. ' + - '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', - ); + + if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) { + schedulePassiveEffectCallback(); } + 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 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.', + ); } } - // 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. - commitUpdateQueue(finishedWork, updateQueue, instance); } - return; - } - case HostRoot: { - // TODO: I think this is now always non-null by the time it reaches the - // commit phase. Consider removing the type check. - const updateQueue: UpdateQueue< - *, - > | null = (finishedWork.updateQueue: any); - if (updateQueue !== null) { - let instance = null; - if (finishedWork.child !== null) { - switch (finishedWork.child.tag) { - case HostComponent: - instance = getPublicInstance(finishedWork.child.stateNode); - break; - case ClassComponent: - instance = finishedWork.child.stateNode; - break; - } + + 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); } - commitUpdateQueue(finishedWork, updateQueue, instance); } - return; + break; } - case HostComponent: { - const instance: Instance = finishedWork.stateNode; + } +} - // Renderers may schedule work to be done after host components are mounted - // (eg DOM renderer may schedule auto-focus for inputs and form controls). - // These effects should only be committed when components are first mounted, - // aka when there is no current/alternate. - if (current === null && finishedWork.flags & Update) { - const type = finishedWork.type; - const props = finishedWork.memoizedProps; - commitMount(instance, type, props, finishedWork); - } +function commitLayoutEffectsForProfiler( + finishedWork: Fiber, + finishedRoot: FiberRoot, +) { + if (enableProfilerTimer) { + const flags = finishedWork.flags; + const current = finishedWork.alternate; - return; - } - case HostText: { - // We have no life-cycles associated with text. - return; - } - case HostPortal: { - // We have no life-cycles associated with portals. - return; - } - case Profiler: { - if (enableProfilerTimer) { - const {onCommit, onRender} = finishedWork.memoizedProps; - const {effectDuration} = finishedWork.stateNode; - const flags = finishedWork.flags; + const {onCommit, onRender} = finishedWork.memoizedProps; + const {effectDuration} = finishedWork.stateNode; - const commitTime = getCommitTime(); + const commitTime = getCommitTime(); + + const OnRenderFlag = Update; + const OnCommitFlag = Callback; - const OnRenderFlag = Update; - const OnCommitFlag = Callback; + if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') { + if (enableSchedulerTracing) { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onRender( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + finishedWork.actualDuration, + finishedWork.treeBaseDuration, + finishedWork.actualStartTime, + commitTime, + ); + } + } + if (enableProfilerCommitHooks) { + if ( + (flags & OnCommitFlag) !== NoFlags && + typeof onCommit === 'function' + ) { + if (enableSchedulerTracing) { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + finishedRoot.memoizedInteractions, + ); + } else { + onCommit( + finishedWork.memoizedProps.id, + current === null ? 'mount' : 'update', + effectDuration, + commitTime, + ); + } + } + } + } +} + +function commitLayoutEffectsForClassComponent(finishedWork: Fiber) { + const instance = finishedWork.stateNode; + const current = finishedWork.alternate; + if (finishedWork.flags & Update) { + if (current === null) { + // 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 ( - (flags & OnRenderFlag) !== NoFlags && - typeof onRender === 'function' + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps ) { - if (enableSchedulerTracing) { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, - finishedRoot.memoizedInteractions, + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'componentDidMount. ' + + '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', ); - } else { - onRender( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - finishedWork.actualDuration, - finishedWork.treeBaseDuration, - finishedWork.actualStartTime, - commitTime, + } + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidMount. ' + + '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', ); } } - - if (enableProfilerCommitHooks) { - if ( - (flags & OnCommitFlag) !== NoFlags && - typeof onCommit === 'function' - ) { - if (enableSchedulerTracing) { - onCommit( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - effectDuration, - commitTime, - finishedRoot.memoizedInteractions, - ); - } else { - onCommit( - finishedWork.memoizedProps.id, - current === null ? 'mount' : 'update', - effectDuration, - commitTime, - ); - } + } + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidMount(); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidMount(); + } + } else { + const prevProps = + finishedWork.elementType === finishedWork.type + ? current.memoizedProps + : resolveDefaultProps(finishedWork.type, current.memoizedProps); + const prevState = current.memoizedState; + // 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 ' + + 'componentDidUpdate. ' + + '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', + ); } - - // 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. - // TODO: Use JS Stack instead - let parentFiber = finishedWork.return; - while (parentFiber !== null) { - if (parentFiber.tag === Profiler) { - const parentStateNode = parentFiber.stateNode; - parentStateNode.effectDuration += effectDuration; - break; - } - parentFiber = parentFiber.return; + if (instance.state !== finishedWork.memoizedState) { + console.error( + 'Expected %s state to match memoized state before ' + + 'componentDidUpdate. ' + + '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', + ); } } } - return; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + finishedWork.mode & ProfileMode + ) { + try { + startLayoutEffectTimer(); + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } finally { + recordLayoutEffectDuration(finishedWork); + } + } else { + instance.componentDidUpdate( + prevProps, + prevState, + instance.__reactInternalSnapshotBeforeUpdate, + ); + } } - case SuspenseComponent: { - commitSuspenseHydrationCallbacks(finishedRoot, finishedWork); - return; + } + + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + if (__DEV__) { + if ( + finishedWork.type === finishedWork.elementType && + !didWarnAboutReassigningProps + ) { + if (instance.props !== finishedWork.memoizedProps) { + console.error( + 'Expected %s props to match memoized props before ' + + 'processing the update queue. ' + + '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 ' + + 'processing the update queue. ' + + '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', + ); + } + } } - case SuspenseListComponent: - case IncompleteClassComponent: - case FundamentalComponent: - case ScopeComponent: - case OffscreenComponent: - case LegacyHiddenComponent: - return; + // 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. + commitUpdateQueue(finishedWork, updateQueue, instance); + } +} + +function commitLayoutEffectsForHostRoot(finishedWork: Fiber) { + // TODO: I think this is now always non-null by the time it reaches the + // commit phase. Consider removing the type check. + const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any); + if (updateQueue !== null) { + let instance = null; + if (finishedWork.child !== null) { + switch (finishedWork.child.tag) { + case HostComponent: + instance = getPublicInstance(finishedWork.child.stateNode); + break; + case ClassComponent: + instance = finishedWork.child.stateNode; + break; + } + } + commitUpdateQueue(finishedWork, updateQueue, instance); + } +} + +function commitLayoutEffectsForHostComponent(finishedWork: Fiber) { + const instance: Instance = finishedWork.stateNode; + const current = finishedWork.alternate; + + // Renderers may schedule work to be done after host components are mounted + // (eg DOM renderer may schedule auto-focus for inputs and form controls). + // These effects should only be committed when components are first mounted, + // aka when there is no current/alternate. + if (current === null && finishedWork.flags & Update) { + const type = finishedWork.type; + const props = finishedWork.memoizedProps; + commitMount(instance, type, props, finishedWork); } - 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 hideOrUnhideAllChildren(finishedWork, isHidden) { @@ -1790,7 +1927,7 @@ function commitResetTextContent(current: Fiber): void { resetTextContent(current.stateNode); } -function commitPassiveUnmountInsideDeletedTree(finishedWork: Fiber): void { +function commitPassiveUnmount(finishedWork: Fiber): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: @@ -1820,7 +1957,7 @@ function commitPassiveUnmountInsideDeletedTree(finishedWork: Fiber): void { } } -function commitPassiveUnmount( +function commitPassiveUnmountInsideDeletedTree( current: Fiber, nearestMountedAncestor: Fiber | null, ): void { @@ -2016,7 +2153,6 @@ export { commitPlacement, commitDeletion, commitWork, - commitLifeCycles, commitAttachRef, commitDetachRef, commitPassiveUnmount, @@ -2026,4 +2162,5 @@ export { invokeLayoutEffectUnmountInDEV, invokePassiveEffectMountInDEV, invokePassiveEffectUnmountInDEV, + recursivelyCommitLayoutEffects, }; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index bd8ef8ce5e3c6..4f40bde182310 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -22,6 +22,7 @@ import { enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, enableProfilerTimer, + enableProfilerCommitHooks, enableSchedulerTracing, warnAboutUnmockedScheduler, deferRenderPhaseUpdateToNextBatch, @@ -116,6 +117,7 @@ import { SimpleMemoComponent, Block, ScopeComponent, + Profiler, } from './ReactWorkTags'; import {LegacyRoot} from './ReactRootTags'; import { @@ -126,7 +128,6 @@ import { Ref, ContentReset, Snapshot, - Callback, Passive, PassiveStatic, Incomplete, @@ -190,7 +191,6 @@ import { } from './ReactFiberThrow.new'; import { commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber, - commitLifeCycles as commitLayoutEffectOnFiber, commitPlacement, commitWork, commitDeletion, @@ -205,6 +205,7 @@ import { invokePassiveEffectMountInDEV, invokeLayoutEffectUnmountInDEV, invokePassiveEffectUnmountInDEV, + recursivelyCommitLayoutEffects, } from './ReactFiberCommitWork.new'; import {enqueueUpdate} from './ReactUpdateQueue.new'; import {resetContextDependencies} from './ReactFiberNewContext.new'; @@ -325,6 +326,9 @@ 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; } @@ -1937,7 +1941,27 @@ function commitRootImpl(root, renderPriorityLevel) { markLayoutEffectsStarted(lanes); } - commitLayoutEffects(finishedWork, root, 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); + } + } if (__DEV__) { if (enableDebugTracing) { @@ -2253,8 +2277,7 @@ function commitMutationEffectsImpl( commitDetachRef(current); } if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. + // TODO: This is a temporary solution that allowed us to transition away from React Flare on www. if (fiber.tag === ScopeComponent) { commitAttachRef(fiber); } @@ -2355,75 +2378,6 @@ export function schedulePassiveEffectCallback() { } } -function commitLayoutEffects( - firstChild: Fiber, - root: FiberRoot, - committedLanes: Lanes, -) { - let fiber = firstChild; - while (fiber !== null) { - if (fiber.child !== null) { - const primarySubtreeFlags = fiber.subtreeFlags & LayoutMask; - if (primarySubtreeFlags !== NoFlags) { - commitLayoutEffects(fiber.child, root, committedLanes); - } - } - - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - invokeGuardedCallback( - null, - commitLayoutEffectsImpl, - null, - fiber, - root, - committedLanes, - ); - if (hasCaughtError()) { - const error = clearCaughtError(); - captureCommitPhaseError(fiber, fiber.return, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - commitLayoutEffectsImpl(fiber, root, committedLanes); - } catch (error) { - captureCommitPhaseError(fiber, fiber.return, error); - } - } - fiber = fiber.sibling; - } -} - -function commitLayoutEffectsImpl( - fiber: Fiber, - root: FiberRoot, - committedLanes: Lanes, -) { - const flags = fiber.flags; - - setCurrentDebugFiberInDEV(fiber); - - if (flags & (Update | Callback)) { - const current = fiber.alternate; - commitLayoutEffectOnFiber(root, current, fiber, committedLanes); - } - - if (enableScopeAPI) { - // TODO: This is a temporary solution that allowed us to transition away - // from React Flare on www. - if (flags & Ref && fiber.tag !== ScopeComponent) { - commitAttachRef(fiber); - } - } else { - if (flags & Ref) { - commitAttachRef(fiber); - } - } - - resetCurrentDebugFiberInDEV(); -} - export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) { @@ -2452,6 +2406,14 @@ export function flushPassiveEffects(): boolean { 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) { @@ -2482,6 +2444,19 @@ function flushPassiveMountEffects(root, firstChild: Fiber): void { } } + 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; } } @@ -2515,7 +2490,7 @@ function flushPassiveUnmountEffects(firstChild: Fiber): void { const primaryFlags = fiber.flags & Passive; if (primaryFlags !== NoFlags) { setCurrentDebugFiberInDEV(fiber); - commitPassiveUnmountInsideDeletedTreeOnFiber(fiber); + commitPassiveUnmountOnFiber(fiber); resetCurrentDebugFiberInDEV(); } @@ -2544,7 +2519,10 @@ function flushPassiveUnmountEffectsInsideOfDeletedTree( if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) { setCurrentDebugFiberInDEV(fiberToDelete); - commitPassiveUnmountOnFiber(fiberToDelete, nearestMountedAncestor); + commitPassiveUnmountInsideDeletedTreeOnFiber( + fiberToDelete, + nearestMountedAncestor, + ); resetCurrentDebugFiberInDEV(); } }