diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index c090768ca1c2e..e39660ba0b562 100644 --- a/packages/react-reconciler/src/ReactChildFiber.new.js +++ b/packages/react-reconciler/src/ReactChildFiber.new.js @@ -15,7 +15,7 @@ import type {Fiber} from './ReactInternalTypes'; import type {Lanes} from './ReactFiberLane'; import getComponentName from 'shared/getComponentName'; -import {Placement, Deletion} from './ReactSideEffectTags'; +import {Deletion, Placement} from './ReactSideEffectTags'; import { getIteratorFn, REACT_ELEMENT_TYPE, diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js index 53133777866cb..60e9379e8ce53 100644 --- a/packages/react-reconciler/src/ReactFiber.new.js +++ b/packages/react-reconciler/src/ReactFiber.new.js @@ -29,7 +29,7 @@ import { enableScopeAPI, enableBlocksAPI, } from 'shared/ReactFeatureFlags'; -import {NoEffect, Placement} from './ReactSideEffectTags'; +import {NoEffect, Placement, StaticMask} from './ReactSideEffectTags'; import {NoEffect as NoSubtreeEffect} from './ReactSubtreeTags'; import {ConcurrentRoot, BlockingRoot} from './ReactRootTags'; import { @@ -288,8 +288,6 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { workInProgress.type = current.type; // We already have an alternate. - // Reset the effect tag. - workInProgress.effectTag = NoEffect; workInProgress.subtreeTag = NoSubtreeEffect; workInProgress.deletions = null; @@ -308,6 +306,9 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { } } + // Reset all effects except static ones. + // Static effects are not specific to a render. + workInProgress.effectTag = current.effectTag & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 4c068edc0fa7b..54962f2365d9f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -2064,7 +2064,6 @@ function updateSuspensePrimaryChildren( if (currentFallbackChildFragment !== null) { // Delete the fallback child fragment currentFallbackChildFragment.nextEffect = null; - currentFallbackChildFragment.effectTag = Deletion; workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment; const deletions = workInProgress.deletions; if (deletions === null) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index d854ae52aa25a..9fc5b1f5b2cf2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -68,6 +68,7 @@ import { Placement, Snapshot, Update, + Passive, } from './ReactSideEffectTags'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; @@ -115,9 +116,8 @@ import { captureCommitPhaseError, resolveRetryWakeable, markCommitTimeOfFallback, - enqueuePendingPassiveHookEffectMount, - enqueuePendingPassiveHookEffectUnmount, enqueuePendingPassiveProfilerEffect, + schedulePassiveEffectCallback, } from './ReactFiberWorkLoop.new'; import { NoEffect as NoHookEffect, @@ -130,6 +130,10 @@ import { updateDeprecatedEventListeners, unmountDeprecatedResponderListeners, } from './ReactFiberDeprecatedEvents.new'; +import { + NoEffect as NoSubtreeTag, + Passive as PassiveSubtreeTag, +} from './ReactSubtreeTags'; let didWarnAboutUndefinedSnapshotBeforeUpdate: Set | null = null; if (__DEV__) { @@ -381,26 +385,6 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) { } } -function schedulePassiveEffects(finishedWork: Fiber) { - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; - if (lastEffect !== null) { - const firstEffect = lastEffect.next; - let effect = firstEffect; - do { - const {next, tag} = effect; - if ( - (tag & HookPassive) !== NoHookEffect && - (tag & HookHasEffect) !== NoHookEffect - ) { - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); - enqueuePendingPassiveHookEffectMount(finishedWork, effect); - } - effect = next; - } while (effect !== firstEffect); - } -} - export function commitPassiveEffectDurations( finishedRoot: FiberRoot, finishedWork: Fiber, @@ -486,7 +470,9 @@ function commitLifeCycles( commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); } - schedulePassiveEffects(finishedWork); + if ((finishedWork.subtreeTag & PassiveSubtreeTag) !== NoSubtreeTag) { + schedulePassiveEffectCallback(); + } return; } case ClassComponent: { @@ -892,7 +878,12 @@ function commitUnmount( const {destroy, tag} = effect; if (destroy !== undefined) { if ((tag & HookPassive) !== NoHookEffect) { - enqueuePendingPassiveHookEffectUnmount(current, effect); + // TODO: Consider if we can move this block out of the synchronous commit phase + effect.tag |= HookHasEffect; + + current.effectTag |= Passive; + + schedulePassiveEffectCallback(); } else { if ( enableProfilerTimer && @@ -1013,29 +1004,24 @@ function commitNestedUnmounts( } function detachFiberMutation(fiber: Fiber) { - // Cut off the return pointers to disconnect it from the tree. Ideally, we - // should clear the child pointer of the parent alternate to let this + // Cut off the return pointer to disconnect it from the tree. + // This enables us to detect and warn against state updates on an unmounted component. + // It also prevents events from bubbling from within disconnected components. + // + // Ideally, we should also clear the child pointer of the parent alternate to let this // get GC:ed but we don't know which for sure which parent is the current - // one so we'll settle for GC:ing the subtree of this child. This child - // itself will be GC:ed when the parent updates the next time. - // Note: we cannot null out sibling here, otherwise it can cause issues - // with findDOMNode and how it requires the sibling field to carry out - // traversal in a later effect. See PR #16820. We now clear the sibling - // field after effects, see: detachFiberAfterEffects. - fiber.alternate = null; - fiber.child = null; - fiber.dependencies = null; - fiber.firstEffect = null; - fiber.lastEffect = null; - fiber.memoizedProps = null; - fiber.memoizedState = null; - fiber.pendingProps = null; - fiber.return = null; - fiber.stateNode = null; - fiber.updateQueue = null; - if (__DEV__) { - fiber._debugOwner = null; + // one so we'll settle for GC:ing the subtree of this child. + // This child itself will be GC:ed when the parent updates the next time. + // + // Note that we can't clear child or sibling pointers yet. + // They're needed for passive effects and for findDOMNode. + // We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects). + const alternate = fiber.alternate; + if (alternate !== null) { + alternate.return = null; + fiber.alternate = null; } + fiber.return = null; } function emptyPortalContainer(current: Fiber) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index c5f766759e0dc..be63958074d77 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -50,6 +50,7 @@ import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.ne import { Update as UpdateEffect, Passive as PassiveEffect, + PassiveStatic as PassiveStaticEffect, } from './ReactSideEffectTags'; import { HasEffect as HookHasEffect, @@ -1270,7 +1271,7 @@ function mountEffect( } } return mountEffectImpl( - UpdateEffect | PassiveEffect, + UpdateEffect | PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, @@ -1631,7 +1632,8 @@ function mountOpaqueIdentifier(): OpaqueIDType | void { const setId = mountState(id)[1]; if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) { - currentlyRenderingFiber.effectTag |= UpdateEffect | PassiveEffect; + currentlyRenderingFiber.effectTag |= + UpdateEffect | PassiveEffect | PassiveStaticEffect; pushEffect( HookHasEffect | HookPassive, () => { diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index 5071f76fb5fe4..cc9965e2ea81a 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -124,7 +124,7 @@ function deleteHydratableInstance( const childToDelete = createFiberFromHostInstanceForDeletion(); childToDelete.stateNode = instance; childToDelete.return = returnFiber; - childToDelete.effectTag = Deletion; + const deletions = returnFiber.deletions; if (deletions === null) { returnFiber.deletions = [childToDelete]; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 0540d53df5534..69992f6807a1e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -16,6 +16,7 @@ import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; import type {SuspenseState} from './ReactFiberSuspenseComponent.new'; import type {Effect as HookEffect} from './ReactFiberHooks.new'; import type {StackCursor} from './ReactFiberStack.new'; +import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new'; import { warnAboutDeprecatedLifecycles, @@ -49,6 +50,11 @@ import { flushSyncCallbackQueue, scheduleSyncCallback, } from './SchedulerWithReactIntegration.new'; +import { + NoEffect as NoHookEffect, + HasEffect as HookHasEffect, + Passive as HookPassive, +} from './ReactHookEffectTags'; import { logCommitStarted, logCommitStopped, @@ -126,7 +132,7 @@ import { Snapshot, Callback, Passive, - PassiveUnmountPendingDev, + PassiveStatic, Incomplete, HostEffectMask, Hydrating, @@ -134,12 +140,14 @@ import { BeforeMutationMask, MutationMask, LayoutMask, + PassiveMask, } from './ReactSideEffectTags'; import { NoEffect as NoSubtreeTag, - BeforeMutation, - Mutation, - Layout, + BeforeMutation as BeforeMutationSubtreeTag, + Mutation as MutationSubtreeTag, + Layout as LayoutSubtreeTag, + Passive as PassiveSubtreeTag, } from './ReactSubtreeTags'; import { NoLanePriority, @@ -327,8 +335,6 @@ let rootDoesHavePassiveEffects: boolean = false; let rootWithPendingPassiveEffects: FiberRoot | null = null; let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority; let pendingPassiveEffectsLanes: Lanes = NoLanes; -let pendingPassiveHookEffectsMount: Array = []; -let pendingPassiveHookEffectsUnmount: Array = []; let pendingPassiveProfilerEffects: Array = []; let rootsWithPendingDiscreteUpdates: Set | null = null; @@ -1882,13 +1888,16 @@ function resetChildLanes(completedWork: Fiber) { const effectTag = child.effectTag; if ((effectTag & BeforeMutationMask) !== NoEffect) { - subtreeTag |= BeforeMutation; + subtreeTag |= BeforeMutationSubtreeTag; } if ((effectTag & MutationMask) !== NoEffect) { - subtreeTag |= Mutation; + subtreeTag |= MutationSubtreeTag; } if ((effectTag & LayoutMask) !== NoEffect) { - subtreeTag |= Layout; + subtreeTag |= LayoutSubtreeTag; + } + if ((effectTag & PassiveMask) !== NoEffect) { + subtreeTag |= PassiveSubtreeTag; } // When a fiber is cloned, its actualDuration is reset to 0. This value will @@ -1929,13 +1938,16 @@ function resetChildLanes(completedWork: Fiber) { const effectTag = child.effectTag; if ((effectTag & BeforeMutationMask) !== NoEffect) { - subtreeTag |= BeforeMutation; + subtreeTag |= BeforeMutationSubtreeTag; } if ((effectTag & MutationMask) !== NoEffect) { - subtreeTag |= Mutation; + subtreeTag |= MutationSubtreeTag; } if ((effectTag & LayoutMask) !== NoEffect) { - subtreeTag |= Layout; + subtreeTag |= LayoutSubtreeTag; + } + if ((effectTag & PassiveMask) !== NoEffect) { + subtreeTag |= PassiveSubtreeTag; } child = child.sibling; @@ -2170,6 +2182,17 @@ function commitRootImpl(root, renderPriorityLevel) { markLayoutEffectsStopped(); } + // If there are pending passive effects, schedule a callback to process them. + if ((finishedWork.subtreeTag & PassiveSubtreeTag) !== NoSubtreeTag) { + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); + } + } + // Tell Scheduler to yield at the end of the frame, so the browser has an // opportunity to paint. requestPaint(); @@ -2201,8 +2224,6 @@ function commitRootImpl(root, renderPriorityLevel) { rootWithPendingPassiveEffects = root; pendingPassiveEffectsLanes = lanes; pendingPassiveEffectsRenderPriority = renderPriorityLevel; - } else { - // TODO (effects) Detach sibling pointers for deleted Fibers } // Read this again, since an effect might have updated it @@ -2312,7 +2333,7 @@ function commitBeforeMutationEffects(firstChild: Fiber) { } if (fiber.child !== null) { - const primarySubtreeTag = fiber.subtreeTag & BeforeMutation; + const primarySubtreeTag = fiber.subtreeTag & BeforeMutationSubtreeTag; if (primarySubtreeTag !== NoSubtreeTag) { commitBeforeMutationEffects(fiber.child); } @@ -2396,19 +2417,13 @@ function commitMutationEffects( ) { let fiber = firstChild; while (fiber !== null) { - if (fiber.deletions !== null) { - commitMutationEffectsDeletions( - fiber.deletions, - root, - renderPriorityLevel, - ); - - // TODO (effects) Don't clear this yet; we may need to cleanup passive effects - fiber.deletions = null; + const deletions = fiber.deletions; + if (deletions !== null) { + commitMutationEffectsDeletions(deletions, root, renderPriorityLevel); } if (fiber.child !== null) { - const primarySubtreeTag = fiber.subtreeTag & Mutation; + const primarySubtreeTag = fiber.subtreeTag & MutationSubtreeTag; if (primarySubtreeTag !== NoSubtreeTag) { commitMutationEffects(fiber.child, root, renderPriorityLevel); } @@ -2538,7 +2553,16 @@ function commitMutationEffectsDeletions( captureCommitPhaseError(childToDelete, error); } } - // Don't clear the Deletion effect yet; we also use it to know when we need to detach refs later. + } +} + +export function schedulePassiveEffectCallback() { + if (!rootDoesHavePassiveEffects) { + rootDoesHavePassiveEffects = true; + scheduleCallback(NormalSchedulerPriority, () => { + flushPassiveEffects(); + return null; + }); } } @@ -2550,7 +2574,7 @@ function commitLayoutEffects( let fiber = firstChild; while (fiber !== null) { if (fiber.child !== null) { - const primarySubtreeTag = fiber.subtreeTag & Layout; + const primarySubtreeTag = fiber.subtreeTag & LayoutSubtreeTag; if (primarySubtreeTag !== NoSubtreeTag) { commitLayoutEffects(fiber.child, root, committedLanes); } @@ -2645,44 +2669,251 @@ export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void { } } -export function enqueuePendingPassiveHookEffectMount( - fiber: Fiber, - effect: HookEffect, -): void { - pendingPassiveHookEffectsMount.push(effect, fiber); - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); +function invokePassiveEffectCreate(effect: HookEffect): void { + const create = effect.create; + effect.destroy = create(); +} + +function flushPassiveMountEffects(firstChild: Fiber): void { + let fiber = firstChild; + while (fiber !== null) { + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag; + + if (fiber.child !== null && primarySubtreeTag !== NoSubtreeTag) { + flushPassiveMountEffects(fiber.child); + } + + if ((fiber.effectTag & Update) !== NoEffect) { + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + flushPassiveMountEffectsImpl(fiber); + } + } + } + + fiber = fiber.sibling; } } -export function enqueuePendingPassiveHookEffectUnmount( - fiber: Fiber, - effect: HookEffect, -): void { - pendingPassiveHookEffectsUnmount.push(effect, fiber); - if (__DEV__) { - fiber.effectTag |= PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag |= PassiveUnmountPendingDev; +function flushPassiveMountEffectsImpl(fiber: Fiber): void { + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback( + null, + invokePassiveEffectCreate, + null, + effect, + ); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback( + null, + invokePassiveEffectCreate, + null, + effect, + ); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + const create = effect.create; + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + effect.destroy = create(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + effect.destroy = create(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + + effect = next; + } while (effect !== firstEffect); + } +} + +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]; + // If this fiber (or anything below it) has passive effects then traverse the subtree. + const primaryEffectTag = fiberToDelete.effectTag & PassiveMask; + const primarySubtreeTag = fiberToDelete.subtreeTag & PassiveSubtreeTag; + if ( + primarySubtreeTag !== NoSubtreeTag || + primaryEffectTag !== NoEffect + ) { + flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete); + } + + // 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 subtreeTag of the current Fiber, + // rather than the subtreeTag/effectsTag of the first child, + // since that would not cover passive effects in siblings. + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag; + if (primarySubtreeTag !== NoSubtreeTag) { + flushPassiveUnmountEffects(child); + } } + + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + const primaryEffectTag = fiber.effectTag & Passive; + if (primaryEffectTag !== NoEffect) { + flushPassiveUnmountEffectsImpl(fiber); + } + } + } + + fiber = fiber.sibling; } - if (!rootDoesHavePassiveEffects) { - rootDoesHavePassiveEffects = true; - scheduleCallback(NormalSchedulerPriority, () => { - flushPassiveEffects(); - return null; - }); +} + +function flushPassiveUnmountEffectsInsideOfDeletedTree( + firstChild: Fiber, +): void { + let fiber = firstChild; + while (fiber !== null) { + const child = fiber.child; + if (child !== null) { + // If any children have passive effects then traverse the subtree. + // Note that this requires checking subtreeTag of the current Fiber, + // rather than the subtreeTag/effectsTag of the first child, + // since that would not cover passive effects in siblings. + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag; + if (primarySubtreeTag !== NoSubtreeTag) { + flushPassiveUnmountEffectsInsideOfDeletedTree(child); + } + } + + switch (fiber.tag) { + case FunctionComponent: + case ForwardRef: + case SimpleMemoComponent: + case Block: { + const primaryEffectTag = fiber.effectTag & Passive; + if (primaryEffectTag !== NoEffect) { + flushPassiveUnmountEffectsImpl(fiber); + } + } + } + + fiber = fiber.sibling; } } -function invokePassiveEffectCreate(effect: HookEffect): void { - const create = effect.create; - effect.destroy = create(); +function flushPassiveUnmountEffectsImpl(fiber: Fiber): void { + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any); + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + let effect = firstEffect; + do { + const {next, tag} = effect; + if ( + (tag & HookPassive) !== NoHookEffect && + (tag & HookHasEffect) !== NoHookEffect + ) { + const destroy = effect.destroy; + effect.destroy = undefined; + + if (typeof destroy === 'function') { + if (__DEV__) { + setCurrentDebugFiberInDEV(fiber); + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + startPassiveEffectTimer(); + invokeGuardedCallback(null, destroy, null); + recordPassiveEffectDuration(fiber); + } else { + invokeGuardedCallback(null, destroy, null); + } + if (hasCaughtError()) { + invariant(fiber !== null, 'Should be working on an effect.'); + const error = clearCaughtError(); + captureCommitPhaseError(fiber, error); + } + resetCurrentDebugFiberInDEV(); + } else { + try { + if ( + enableProfilerTimer && + enableProfilerCommitHooks && + fiber.mode & ProfileMode + ) { + try { + startPassiveEffectTimer(); + destroy(); + } finally { + recordPassiveEffectDuration(fiber); + } + } else { + destroy(); + } + } catch (error) { + invariant(fiber !== null, 'Should be working on an effect.'); + captureCommitPhaseError(fiber, error); + } + } + } + } + + effect = next; + } while (effect !== firstEffect); + } } function flushPassiveEffectsImpl() { @@ -2724,117 +2955,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. - - // First pass: Destroy stale passive effects. - const unmountEffects = pendingPassiveHookEffectsUnmount; - pendingPassiveHookEffectsUnmount = []; - for (let i = 0; i < unmountEffects.length; i += 2) { - const effect = ((unmountEffects[i]: any): HookEffect); - const fiber = ((unmountEffects[i + 1]: any): Fiber); - const destroy = effect.destroy; - effect.destroy = undefined; - - if (__DEV__) { - fiber.effectTag &= ~PassiveUnmountPendingDev; - const alternate = fiber.alternate; - if (alternate !== null) { - alternate.effectTag &= ~PassiveUnmountPendingDev; - } - } - - if (typeof destroy === 'function') { - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, destroy, null); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, destroy, null); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - destroy(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - destroy(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - } - // Second pass: Create new passive effects. - const mountEffects = pendingPassiveHookEffectsMount; - pendingPassiveHookEffectsMount = []; - for (let i = 0; i < mountEffects.length; i += 2) { - const effect = ((mountEffects[i]: any): HookEffect); - const fiber = ((mountEffects[i + 1]: any): Fiber); - if (__DEV__) { - setCurrentDebugFiberInDEV(fiber); - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - startPassiveEffectTimer(); - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - recordPassiveEffectDuration(fiber); - } else { - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect); - } - if (hasCaughtError()) { - invariant(fiber !== null, 'Should be working on an effect.'); - const error = clearCaughtError(); - captureCommitPhaseError(fiber, error); - } - resetCurrentDebugFiberInDEV(); - } else { - try { - const create = effect.create; - if ( - enableProfilerTimer && - enableProfilerCommitHooks && - fiber.mode & ProfileMode - ) { - try { - startPassiveEffectTimer(); - effect.destroy = create(); - } finally { - recordPassiveEffectDuration(fiber); - } - } else { - effect.destroy = create(); - } - } catch (error) { - invariant(fiber !== null, 'Should be working on an effect.'); - captureCommitPhaseError(fiber, error); - } - } - } - - // TODO (effects) Detach sibling pointers for deleted Fibers + flushPassiveUnmountEffects(root.current); + flushPassiveMountEffects(root.current); if (enableProfilerTimer && enableProfilerCommitHooks) { const profilerEffects = pendingPassiveProfilerEffects; @@ -3230,10 +3352,24 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) { return; } - // If there are pending passive effects unmounts for this Fiber, - // we can assume that they would have prevented this update. - if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) { - return; + if ((fiber.effectTag & PassiveStatic) !== NoEffect) { + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + let effect = firstEffect; + do { + if (effect.destroy !== undefined) { + if ((effect.tag & HookPassive) !== NoHookEffect) { + return; + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } } // We show the whole stack but dedupe on the top component's name because @@ -3934,3 +4070,23 @@ 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.firstEffect = null; + fiber.lastEffect = 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/react-reconciler/src/ReactSideEffectTags.js b/packages/react-reconciler/src/ReactSideEffectTags.js index 8b63f2f22f8ca..2af6d5369a39f 100644 --- a/packages/react-reconciler/src/ReactSideEffectTags.js +++ b/packages/react-reconciler/src/ReactSideEffectTags.js @@ -10,36 +10,50 @@ export type SideEffectTag = number; // Don't change these two values. They're used by React Dev Tools. -export const NoEffect = /* */ 0b000000000000000; -export const PerformedWork = /* */ 0b000000000000001; +export const NoEffect = /* */ 0b0000000000000000; +export const PerformedWork = /* */ 0b0000000000000001; // You can change the rest (and add more). -export const Placement = /* */ 0b000000000000010; -export const Update = /* */ 0b000000000000100; -export const PlacementAndUpdate = /* */ 0b000000000000110; -export const Deletion = /* */ 0b000000000001000; -export const ContentReset = /* */ 0b000000000010000; -export const Callback = /* */ 0b000000000100000; -export const DidCapture = /* */ 0b000000001000000; -export const Ref = /* */ 0b000000010000000; -export const Snapshot = /* */ 0b000000100000000; -export const Passive = /* */ 0b000001000000000; -export const PassiveUnmountPendingDev = /* */ 0b010000000000000; -export const Hydrating = /* */ 0b000010000000000; -export const HydratingAndUpdate = /* */ 0b000010000000100; +export const Placement = /* */ 0b0000000000000010; +export const Update = /* */ 0b0000000000000100; +export const PlacementAndUpdate = /* */ 0b0000000000000110; +export const Deletion = /* */ 0b0000000000001000; +export const ContentReset = /* */ 0b0000000000010000; +export const Callback = /* */ 0b0000000000100000; +export const DidCapture = /* */ 0b0000000001000000; +export const Ref = /* */ 0b0000000010000000; +export const Snapshot = /* */ 0b0000000100000000; +export const Passive = /* */ 0b0000001000000000; +// TODO (effects) Remove this bit once the new reconciler is synced to the old. +export const PassiveUnmountPendingDev = /* */ 0b0010000000000000; +export const Hydrating = /* */ 0b0000010000000000; +export const HydratingAndUpdate = /* */ 0b0000010000000100; // Passive & Update & Callback & Ref & Snapshot -export const LifecycleEffectMask = /* */ 0b000001110100100; +export const LifecycleEffectMask = /* */ 0b0000001110100100; // Union of all host effects -export const HostEffectMask = /* */ 0b000011111111111; +export const HostEffectMask = /* */ 0b0000011111111111; // These are not really side effects, but we still reuse this field. -export const Incomplete = /* */ 0b000100000000000; -export const ShouldCapture = /* */ 0b001000000000000; -export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000; +export const Incomplete = /* */ 0b0000100000000000; +export const ShouldCapture = /* */ 0b0001000000000000; +export const ForceUpdateForLegacySuspense = /* */ 0b0100000000000000; + +// Static tags describe aspects of a fiber that are not specific to a render, +// e.g. a fiber uses a passive effect (even if there are no updates on this particular render). +// This enables us to defer more work in the unmount case, +// since we can defer traversing the tree during layout to look for Passive effects, +// and instead rely on the static flag as a signal that there may be cleanup work. +export const PassiveStatic = /* */ 0b1000000000000000; // Union of side effect groupings as pertains to subtreeTag -export const BeforeMutationMask = /* */ 0b000001100001010; -export const MutationMask = /* */ 0b000010010011110; -export const LayoutMask = /* */ 0b000000010100100; +export const BeforeMutationMask = /* */ 0b0000001100001010; +export const MutationMask = /* */ 0b0000010010011110; +export const LayoutMask = /* */ 0b0000000010100100; +export const PassiveMask = /* */ 0b1000001000001000; + +// Union of tags that don't get reset on clones. +// This allows certain concepts to persist without recalculting them, +// e.g. whether a subtree contains passive effects or portals. +export const StaticMask = /* */ 0b1000000000000000; diff --git a/packages/react-reconciler/src/ReactSubtreeTags.js b/packages/react-reconciler/src/ReactSubtreeTags.js index 7cc1d7acd090b..536d9e074b448 100644 --- a/packages/react-reconciler/src/ReactSubtreeTags.js +++ b/packages/react-reconciler/src/ReactSubtreeTags.js @@ -9,7 +9,8 @@ export type SubtreeTag = number; -export const NoEffect = /* */ 0b000; -export const BeforeMutation = /* */ 0b001; -export const Mutation = /* */ 0b010; -export const Layout = /* */ 0b100; +export const NoEffect = /* */ 0b0000; +export const BeforeMutation = /* */ 0b0001; +export const Mutation = /* */ 0b0010; +export const Layout = /* */ 0b0100; +export const Passive = /* */ 0b1000; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 3c7440502957c..8c709cb593971 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1454,7 +1454,7 @@ describe('ReactSuspense', () => { ]); }); - it('should call onInteractionScheduledWorkCompleted after suspending', done => { + it('should call onInteractionScheduledWorkCompleted after suspending', () => { const subscriber = { onInteractionScheduledWorkCompleted: jest.fn(), onInteractionTraced: jest.fn(), @@ -1512,13 +1512,11 @@ describe('ReactSuspense', () => { jest.advanceTimersByTime(1000); expect(Scheduler).toHaveYielded(['Promise resolved [C]']); - expect(Scheduler).toFlushExpired([ + expect(Scheduler).toFlushAndYield([ // Even though the promise for C was thrown three times, we should only // re-render once. 'C', ]); - - done(); }); expect( diff --git a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js index 8086079b56f3a..4a184a8157438 100644 --- a/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js +++ b/packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js @@ -486,24 +486,51 @@ describe('SchedulingProfiler', () => { ReactTestRenderer.create(, {unstable_isConcurrent: true}); }); - expect(marks.map(normalizeCodeLocInfo)).toEqual([ - '--schedule-render-512', - '--render-start-512', - '--render-stop', - '--commit-start-512', - '--layout-effects-start-512', - '--layout-effects-stop', - '--commit-stop', - '--passive-effects-start-512', - toggleComponentStacks( - '--schedule-state-update-1024-Example-\n in Example (at **)', - ), - '--passive-effects-stop', - '--render-start-1024', - '--render-stop', - '--commit-start-1024', - '--commit-stop', - ]); + gate(({old}) => { + if (old) { + expect(marks.map(normalizeCodeLocInfo)).toEqual([ + '--schedule-render-512', + '--render-start-512', + '--render-stop', + '--commit-start-512', + '--layout-effects-start-512', + '--layout-effects-stop', + '--commit-stop', + '--passive-effects-start-512', + toggleComponentStacks( + '--schedule-state-update-1024-Example-\n in Example (at **)', + ), + '--passive-effects-stop', + '--render-start-1024', + '--render-stop', + '--commit-start-1024', + '--commit-stop', + ]); + } else { + expect(marks.map(normalizeCodeLocInfo)).toEqual([ + '--schedule-render-512', + '--render-start-512', + '--render-stop', + '--commit-start-512', + '--layout-effects-start-512', + '--layout-effects-stop', + '--commit-stop', + '--passive-effects-start-512', + toggleComponentStacks( + '--schedule-state-update-1024-Example-\n in Example (at **)', + ), + '--passive-effects-stop', + '--render-start-1024', + '--render-stop', + '--commit-start-1024', + '--layout-effects-start-1024', + '--layout-effects-stop', + '--commit-stop', + '--passive-effects-start-1024', + '--passive-effects-stop', + ]); + } + }); }); // @gate enableSchedulingProfiler