Convert snapshot phase to depth-first traversal (#20622)

This commit is contained in:
Andrew Clark
2021-01-19 15:37:00 -08:00
committed by GitHub
parent fb3e158a64
commit 2a646f73e4
3 changed files with 167 additions and 111 deletions
@@ -75,9 +75,10 @@ import {
Hydrating,
HydratingAndUpdate,
Passive,
BeforeMutationMask,
MutationMask,
PassiveMask,
LayoutMask,
PassiveMask,
PassiveUnmountPendingDev,
} from './ReactFiberFlags';
import getComponentName from 'shared/getComponentName';
@@ -128,6 +129,8 @@ import {
commitHydratedSuspenseInstance,
clearContainer,
prepareScopeUpdate,
prepareForCommit,
beforeActiveInstanceBlur,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -144,6 +147,7 @@ import {
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
import {doesFiberContain} from './ReactFiberTreeReflection';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
if (__DEV__) {
@@ -259,18 +263,114 @@ function safelyCallDestroy(current: Fiber, destroy: () => void) {
}
}
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
let focusedInstanceHandle: null | Fiber = null;
let shouldFireAfterActiveInstanceBlur: boolean = false;
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
// We no longer need to track the active instance fiber
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
commitBeforeMutationEffectsDeletion(deletion);
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
ensureCorrectReturnPointer(child, fiber);
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
invokeGuardedCallback(
null,
commitBeforeMutationEffectsOnFiber,
null,
fiber,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(fiber, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, error);
}
}
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
nextEffect = fiber.return;
}
}
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
finishedWork.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, finishedWork) &&
doesFiberContain(finishedWork, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(finishedWork);
}
}
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(finishedWork);
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
@@ -324,30 +424,43 @@ function commitBeforeMutationLifeCycles(
}
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.flags & Snapshot) {
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
break;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break;
default: {
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
return;
resetCurrentDebugFiberInDEV();
}
}
function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
// TODO (effects) It would be nice to avoid calling doesFiberContain()
// Maybe we can repurpose one of the subtreeFlags positions for this instead?
// Use it to store which part of the tree the focused instance is in?
// This assumes we can safely determine that instance during the "render" phase.
if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(deletion);
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
@@ -2353,7 +2466,6 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) {
}
export {
commitBeforeMutationLifeCycles,
commitResetTextContent,
commitPlacement,
commitDeletion,
+3
View File
@@ -60,6 +60,9 @@ export const MountPassiveDev = /* */ 0b10000000000000000000;
// don't contain effects, by checking subtreeFlags.
export const BeforeMutationMask =
// TODO: Remove Update flag from before mutation phase by re-landing Visiblity
// flag logic (see #20043)
Update |
Snapshot |
(enableCreateEventHandleAPI
? // createEventHandle needs to visit deleted and hidden trees to
@@ -83,13 +83,11 @@ import * as Scheduler from 'scheduler';
import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
import {
prepareForCommit,
resetAfterCommit,
scheduleTimeout,
cancelTimeout,
noTimeout,
warnsIfNotActing,
beforeActiveInstanceBlur,
afterActiveInstanceBlur,
clearContainer,
} from './ReactFiberHostConfig';
@@ -122,9 +120,7 @@ import {
NoFlags,
PerformedWork,
Placement,
Deletion,
ChildDeletion,
Snapshot,
Passive,
PassiveStatic,
Incomplete,
@@ -181,11 +177,10 @@ import {
createClassErrorUpdate,
} from './ReactFiberThrow.new';
import {
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
commitBeforeMutationEffects,
commitLayoutEffects,
commitMutationEffects,
commitPassiveEffectDurations,
isSuspenseBoundaryBeingHidden,
commitPassiveMountEffects,
commitPassiveUnmountEffects,
detachFiberAfterEffects,
@@ -232,7 +227,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
// Used by `act`
import enqueueTask from 'shared/enqueueTask';
import {doesFiberContain} from './ReactFiberTreeReflection';
const ceil = Math.ceil;
@@ -363,9 +357,6 @@ let currentEventPendingLanes: Lanes = NoLanes;
// We warn about state updates for unmounted components differently in this case.
let isFlushingPassiveEffects = false;
let focusedInstanceHandle: null | Fiber = null;
let shouldFireAfterActiveInstanceBlur: boolean = false;
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
@@ -1941,6 +1932,24 @@ function commitRootImpl(root, renderPriorityLevel) {
firstEffect = finishedWork.firstEffect;
}
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
if (
(finishedWork.subtreeFlags & Passive) !== NoFlags ||
(finishedWork.flags & Passive) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
if (firstEffect !== null) {
let previousLanePriority;
if (decoupleUpdatePriorityFromScheduler) {
@@ -1962,32 +1971,10 @@ function commitRootImpl(root, renderPriorityLevel) {
// The first phase a "before mutation" phase. We use this phase to read the
// state of the host tree right before we mutate it. This is where
// getSnapshotBeforeUpdate is called.
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
nextEffect = firstEffect;
do {
if (__DEV__) {
invokeGuardedCallback(null, commitBeforeMutationEffects, null);
if (hasCaughtError()) {
invariant(nextEffect !== null, 'Should be working on an effect.');
const error = clearCaughtError();
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} else {
try {
commitBeforeMutationEffects();
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
}
} while (nextEffect !== null);
// We no longer need to track the active instance fiber
focusedInstanceHandle = null;
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
if (enableProfilerTimer) {
// Mark the current commit time to be shared by all Profilers in this
@@ -2204,52 +2191,6 @@ function commitRootImpl(root, renderPriorityLevel) {
return null;
}
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
if ((nextEffect.flags & Deletion) !== NoFlags) {
if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(nextEffect);
}
} else {
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
nextEffect.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, nextEffect) &&
doesFiberContain(nextEffect, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true;
beforeActiveInstanceBlur(nextEffect);
}
}
}
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(nextEffect);
commitBeforeMutationEffectOnFiber(current, nextEffect);
resetCurrentDebugFiberInDEV();
}
if ((flags & Passive) !== NoFlags) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
export function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {