mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Effects list refactor continued: passive effects traversal (#19374)
* Adds new `Passive` subtree tag value. * Adds recursive traversal for passive effects (mounts and unmounts). * Removes `pendingPassiveHookEffectsMount` and `pendingPassiveHookEffectsUnmount` arrays from work loop. * Re-adds sibling and child pointer detaching (temporarily removed in previous PR). * Addresses some minor TODO comments left over from previous PRs. --- Co-authored-by: Luna Ruan <luna@fb.com>
This commit is contained in:
co-authored by
Luna Ruan
parent
74cd7e5f17
commit
eae90cdbe9
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<mixed> | 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) {
|
||||
|
||||
@@ -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,
|
||||
() => {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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<HookEffect | Fiber> = [];
|
||||
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
|
||||
let pendingPassiveProfilerEffects: Array<Fiber> = [];
|
||||
|
||||
let rootsWithPendingDiscreteUpdates: Set<FiberRoot> | 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,46 +2669,253 @@ 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 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() {
|
||||
if (rootWithPendingPassiveEffects === null) {
|
||||
return false;
|
||||
@@ -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<mixed>): Thenable<void> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+37
-23
@@ -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;
|
||||
|
||||
+5
-4
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -486,24 +486,51 @@ describe('SchedulingProfiler', () => {
|
||||
ReactTestRenderer.create(<Example />, {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
|
||||
|
||||
Reference in New Issue
Block a user