mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
113497cc0e
* Track most recent commit time of a fallback globally This value is going to be used to avoid committing too many fallback states in quick succession. It doesn't really matter where in the tree that happened. This means that we now don't really need the concept of SuspenseState other than has a flag. It could be made cheaper/simpler. * Change suspense heuristic This now eagerly commits non-delayed suspended trees, unless they're only retries in which case they're throttled to 500ms. * Restart early if we're going to suspend later * Use the local variable where appropriate * Make ReactLazy tests less specific on asserting intermediate states They're not testing the exact states of the suspense boundaries, only the result. I keep assertions that they're not already resolved early. * Adjust Profiler tests to the new heuristics * Update snapshot tests for user timing tests I also added a blank initial render to ensuree that we cover the suspended case. * Adjust Suspense tests to account for new heuristics Mostly this just means render the Suspense boundary first so that it becomes an update instead of initial mount. * Track whether we have a ping on the currently rendering level If we get a ping on this level but have not yet suspended, we might still suspend later. In that case we should still restart. * Add comment about moving markers We should add this to throwException so we get these markers earlier. I've had to rewrite tests that test restarting to account for the delayed restarting heuristic. Ideally, we should also be able to restart from within throwException if we're already ready to restart. Right now we wait until the next yield. * Add test for restarting during throttled retry * Add test that we don't restart for initial render * Add Suspense Heuristics as a comment in Throw
1335 lines
42 KiB
JavaScript
1335 lines
42 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {
|
|
Instance,
|
|
TextInstance,
|
|
SuspenseInstance,
|
|
Container,
|
|
ChildSet,
|
|
UpdatePayload,
|
|
} from './ReactFiberHostConfig';
|
|
import type {Fiber} from './ReactFiber';
|
|
import type {FiberRoot} from './ReactFiberRoot';
|
|
import type {ExpirationTime} from './ReactFiberExpirationTime';
|
|
import type {CapturedValue, CapturedError} from './ReactCapturedValue';
|
|
import type {SuspenseState} from './ReactFiberSuspenseComponent';
|
|
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
|
|
import type {Thenable} from './ReactFiberWorkLoop';
|
|
|
|
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
|
|
import {
|
|
enableSchedulerTracing,
|
|
enableProfilerTimer,
|
|
enableSuspenseServerRenderer,
|
|
enableEventAPI,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {
|
|
FunctionComponent,
|
|
ForwardRef,
|
|
ClassComponent,
|
|
HostRoot,
|
|
HostComponent,
|
|
HostText,
|
|
HostPortal,
|
|
Profiler,
|
|
SuspenseComponent,
|
|
DehydratedSuspenseComponent,
|
|
IncompleteClassComponent,
|
|
MemoComponent,
|
|
SimpleMemoComponent,
|
|
EventComponent,
|
|
EventTarget,
|
|
} from 'shared/ReactWorkTags';
|
|
import {
|
|
invokeGuardedCallback,
|
|
hasCaughtError,
|
|
clearCaughtError,
|
|
} from 'shared/ReactErrorUtils';
|
|
import {
|
|
ContentReset,
|
|
Placement,
|
|
Snapshot,
|
|
Update,
|
|
} from 'shared/ReactSideEffectTags';
|
|
import getComponentName from 'shared/getComponentName';
|
|
import invariant from 'shared/invariant';
|
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
|
import warning from 'shared/warning';
|
|
|
|
import {onCommitUnmount} from './ReactFiberDevToolsHook';
|
|
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
|
|
import {getStackByFiberInDevAndProd} from './ReactCurrentFiber';
|
|
import {logCapturedError} from './ReactFiberErrorLogger';
|
|
import {resolveDefaultProps} from './ReactFiberLazyComponent';
|
|
import {getCommitTime} from './ReactProfilerTimer';
|
|
import {commitUpdateQueue} from './ReactUpdateQueue';
|
|
import {
|
|
getPublicInstance,
|
|
supportsMutation,
|
|
supportsPersistence,
|
|
commitMount,
|
|
commitUpdate,
|
|
resetTextContent,
|
|
commitTextUpdate,
|
|
appendChild,
|
|
appendChildToContainer,
|
|
insertBefore,
|
|
insertInContainerBefore,
|
|
removeChild,
|
|
removeChildFromContainer,
|
|
clearSuspenseBoundary,
|
|
clearSuspenseBoundaryFromContainer,
|
|
replaceContainerChildren,
|
|
createContainerChildSet,
|
|
hideInstance,
|
|
hideTextInstance,
|
|
unhideInstance,
|
|
unhideTextInstance,
|
|
unmountEventComponent,
|
|
commitEventTarget,
|
|
mountEventComponent,
|
|
} from './ReactFiberHostConfig';
|
|
import {
|
|
captureCommitPhaseError,
|
|
resolveRetryThenable,
|
|
markCommitTimeOfFallback,
|
|
} from './ReactFiberWorkLoop';
|
|
import {
|
|
NoEffect as NoHookEffect,
|
|
UnmountSnapshot,
|
|
UnmountMutation,
|
|
MountMutation,
|
|
UnmountLayout,
|
|
MountLayout,
|
|
UnmountPassive,
|
|
MountPassive,
|
|
} from './ReactHookEffectTags';
|
|
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
|
|
|
|
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
|
|
if (__DEV__) {
|
|
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
|
|
}
|
|
|
|
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
|
|
|
|
export function logError(boundary: Fiber, errorInfo: CapturedValue<mixed>) {
|
|
const source = errorInfo.source;
|
|
let stack = errorInfo.stack;
|
|
if (stack === null && source !== null) {
|
|
stack = getStackByFiberInDevAndProd(source);
|
|
}
|
|
|
|
const capturedError: CapturedError = {
|
|
componentName: source !== null ? getComponentName(source.type) : null,
|
|
componentStack: stack !== null ? stack : '',
|
|
error: errorInfo.value,
|
|
errorBoundary: null,
|
|
errorBoundaryName: null,
|
|
errorBoundaryFound: false,
|
|
willRetry: false,
|
|
};
|
|
|
|
if (boundary !== null && boundary.tag === ClassComponent) {
|
|
capturedError.errorBoundary = boundary.stateNode;
|
|
capturedError.errorBoundaryName = getComponentName(boundary.type);
|
|
capturedError.errorBoundaryFound = true;
|
|
capturedError.willRetry = true;
|
|
}
|
|
|
|
try {
|
|
logCapturedError(capturedError);
|
|
} catch (e) {
|
|
// This method must not throw, or React internal state will get messed up.
|
|
// If console.error is overridden, or logCapturedError() shows a dialog that throws,
|
|
// we want to report this error outside of the normal stack as a last resort.
|
|
// https://github.com/facebook/react/issues/13188
|
|
setTimeout(() => {
|
|
throw e;
|
|
});
|
|
}
|
|
}
|
|
|
|
const callComponentWillUnmountWithTimer = function(current, instance) {
|
|
startPhaseTimer(current, 'componentWillUnmount');
|
|
instance.props = current.memoizedProps;
|
|
instance.state = current.memoizedState;
|
|
instance.componentWillUnmount();
|
|
stopPhaseTimer();
|
|
};
|
|
|
|
// Capture errors so they don't interrupt unmounting.
|
|
function safelyCallComponentWillUnmount(current, instance) {
|
|
if (__DEV__) {
|
|
invokeGuardedCallback(
|
|
null,
|
|
callComponentWillUnmountWithTimer,
|
|
null,
|
|
current,
|
|
instance,
|
|
);
|
|
if (hasCaughtError()) {
|
|
const unmountError = clearCaughtError();
|
|
captureCommitPhaseError(current, unmountError);
|
|
}
|
|
} else {
|
|
try {
|
|
callComponentWillUnmountWithTimer(current, instance);
|
|
} catch (unmountError) {
|
|
captureCommitPhaseError(current, unmountError);
|
|
}
|
|
}
|
|
}
|
|
|
|
function safelyDetachRef(current: Fiber) {
|
|
const ref = current.ref;
|
|
if (ref !== null) {
|
|
if (typeof ref === 'function') {
|
|
if (__DEV__) {
|
|
invokeGuardedCallback(null, ref, null, null);
|
|
if (hasCaughtError()) {
|
|
const refError = clearCaughtError();
|
|
captureCommitPhaseError(current, refError);
|
|
}
|
|
} else {
|
|
try {
|
|
ref(null);
|
|
} catch (refError) {
|
|
captureCommitPhaseError(current, refError);
|
|
}
|
|
}
|
|
} else {
|
|
ref.current = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
function safelyCallDestroy(current, destroy) {
|
|
if (__DEV__) {
|
|
invokeGuardedCallback(null, destroy, null);
|
|
if (hasCaughtError()) {
|
|
const error = clearCaughtError();
|
|
captureCommitPhaseError(current, error);
|
|
}
|
|
} else {
|
|
try {
|
|
destroy();
|
|
} catch (error) {
|
|
captureCommitPhaseError(current, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitBeforeMutationLifeCycles(
|
|
current: Fiber | null,
|
|
finishedWork: Fiber,
|
|
): void {
|
|
switch (finishedWork.tag) {
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case SimpleMemoComponent: {
|
|
commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
|
|
return;
|
|
}
|
|
case ClassComponent: {
|
|
if (finishedWork.effectTag & Snapshot) {
|
|
if (current !== null) {
|
|
const prevProps = current.memoizedProps;
|
|
const prevState = current.memoizedState;
|
|
startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
|
|
const instance = finishedWork.stateNode;
|
|
// We could update instance props and state here,
|
|
// but instead we rely on them being set during last render.
|
|
// TODO: revisit this when we implement resuming.
|
|
if (__DEV__) {
|
|
if (
|
|
finishedWork.type === finishedWork.elementType &&
|
|
!didWarnAboutReassigningProps
|
|
) {
|
|
warning(
|
|
instance.props === finishedWork.memoizedProps,
|
|
'Expected %s props to match memoized props before ' +
|
|
'getSnapshotBeforeUpdate. ' +
|
|
'This might either be because of a bug in React, or because ' +
|
|
'a component reassigns its own `this.props`. ' +
|
|
'Please file an issue.',
|
|
getComponentName(finishedWork.type) || 'instance',
|
|
);
|
|
warning(
|
|
instance.state === finishedWork.memoizedState,
|
|
'Expected %s state to match memoized state before ' +
|
|
'getSnapshotBeforeUpdate. ' +
|
|
'This might either be because of a bug in React, or because ' +
|
|
'a component reassigns its own `this.props`. ' +
|
|
'Please file an issue.',
|
|
getComponentName(finishedWork.type) || 'instance',
|
|
);
|
|
}
|
|
}
|
|
const snapshot = instance.getSnapshotBeforeUpdate(
|
|
finishedWork.elementType === finishedWork.type
|
|
? prevProps
|
|
: resolveDefaultProps(finishedWork.type, prevProps),
|
|
prevState,
|
|
);
|
|
if (__DEV__) {
|
|
const didWarnSet = ((didWarnAboutUndefinedSnapshotBeforeUpdate: any): Set<
|
|
mixed,
|
|
>);
|
|
if (snapshot === undefined && !didWarnSet.has(finishedWork.type)) {
|
|
didWarnSet.add(finishedWork.type);
|
|
warningWithoutStack(
|
|
false,
|
|
'%s.getSnapshotBeforeUpdate(): A snapshot value (or null) ' +
|
|
'must be returned. You have returned undefined.',
|
|
getComponentName(finishedWork.type),
|
|
);
|
|
}
|
|
}
|
|
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
|
|
stopPhaseTimer();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case HostRoot:
|
|
case HostComponent:
|
|
case HostText:
|
|
case HostPortal:
|
|
case IncompleteClassComponent:
|
|
case EventTarget:
|
|
// Nothing to do for these component types
|
|
return;
|
|
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.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitHookEffectList(
|
|
unmountTag: number,
|
|
mountTag: number,
|
|
finishedWork: Fiber,
|
|
) {
|
|
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
|
|
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
|
|
if (lastEffect !== null) {
|
|
const firstEffect = lastEffect.next;
|
|
let effect = firstEffect;
|
|
do {
|
|
if ((effect.tag & unmountTag) !== NoHookEffect) {
|
|
// Unmount
|
|
const destroy = effect.destroy;
|
|
effect.destroy = undefined;
|
|
if (destroy !== undefined) {
|
|
destroy();
|
|
}
|
|
}
|
|
if ((effect.tag & mountTag) !== NoHookEffect) {
|
|
// Mount
|
|
const create = effect.create;
|
|
effect.destroy = create();
|
|
|
|
if (__DEV__) {
|
|
const destroy = effect.destroy;
|
|
if (destroy !== undefined && typeof destroy !== 'function') {
|
|
let addendum;
|
|
if (destroy === null) {
|
|
addendum =
|
|
' You returned null. If your effect does not require clean ' +
|
|
'up, return undefined (or nothing).';
|
|
} else if (typeof destroy.then === 'function') {
|
|
addendum =
|
|
'\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' +
|
|
'Instead, write the async function inside your effect ' +
|
|
'and call it immediately:\n\n' +
|
|
'useEffect(() => {\n' +
|
|
' async function fetchData() {\n' +
|
|
' // You can await here\n' +
|
|
' const response = await MyAPI.getData(someId);\n' +
|
|
' // ...\n' +
|
|
' }\n' +
|
|
' fetchData();\n' +
|
|
`}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
|
|
'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching';
|
|
} else {
|
|
addendum = ' You returned: ' + destroy;
|
|
}
|
|
warningWithoutStack(
|
|
false,
|
|
'An effect function must not return anything besides a function, ' +
|
|
'which is used for clean-up.%s%s',
|
|
addendum,
|
|
getStackByFiberInDevAndProd(finishedWork),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
effect = effect.next;
|
|
} while (effect !== firstEffect);
|
|
}
|
|
}
|
|
|
|
export function commitPassiveHookEffects(finishedWork: Fiber): void {
|
|
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
|
|
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
|
|
}
|
|
|
|
function commitLifeCycles(
|
|
finishedRoot: FiberRoot,
|
|
current: Fiber | null,
|
|
finishedWork: Fiber,
|
|
committedExpirationTime: ExpirationTime,
|
|
): void {
|
|
switch (finishedWork.tag) {
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case SimpleMemoComponent: {
|
|
commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
|
|
break;
|
|
}
|
|
case ClassComponent: {
|
|
const instance = finishedWork.stateNode;
|
|
if (finishedWork.effectTag & Update) {
|
|
if (current === null) {
|
|
startPhaseTimer(finishedWork, 'componentDidMount');
|
|
// 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
|
|
) {
|
|
warning(
|
|
instance.props === finishedWork.memoizedProps,
|
|
'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',
|
|
);
|
|
warning(
|
|
instance.state === finishedWork.memoizedState,
|
|
'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.props`. ' +
|
|
'Please file an issue.',
|
|
getComponentName(finishedWork.type) || 'instance',
|
|
);
|
|
}
|
|
}
|
|
instance.componentDidMount();
|
|
stopPhaseTimer();
|
|
} else {
|
|
const prevProps =
|
|
finishedWork.elementType === finishedWork.type
|
|
? current.memoizedProps
|
|
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
|
|
const prevState = current.memoizedState;
|
|
startPhaseTimer(finishedWork, 'componentDidUpdate');
|
|
// 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
|
|
) {
|
|
warning(
|
|
instance.props === finishedWork.memoizedProps,
|
|
'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',
|
|
);
|
|
warning(
|
|
instance.state === finishedWork.memoizedState,
|
|
'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.props`. ' +
|
|
'Please file an issue.',
|
|
getComponentName(finishedWork.type) || 'instance',
|
|
);
|
|
}
|
|
}
|
|
instance.componentDidUpdate(
|
|
prevProps,
|
|
prevState,
|
|
instance.__reactInternalSnapshotBeforeUpdate,
|
|
);
|
|
stopPhaseTimer();
|
|
}
|
|
}
|
|
const updateQueue = finishedWork.updateQueue;
|
|
if (updateQueue !== null) {
|
|
if (__DEV__) {
|
|
if (
|
|
finishedWork.type === finishedWork.elementType &&
|
|
!didWarnAboutReassigningProps
|
|
) {
|
|
warning(
|
|
instance.props === finishedWork.memoizedProps,
|
|
'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',
|
|
);
|
|
warning(
|
|
instance.state === finishedWork.memoizedState,
|
|
'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.props`. ' +
|
|
'Please file an issue.',
|
|
getComponentName(finishedWork.type) || 'instance',
|
|
);
|
|
}
|
|
}
|
|
// 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,
|
|
committedExpirationTime,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
case HostRoot: {
|
|
const updateQueue = finishedWork.updateQueue;
|
|
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,
|
|
committedExpirationTime,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
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.effectTag & Update) {
|
|
const type = finishedWork.type;
|
|
const props = finishedWork.memoizedProps;
|
|
commitMount(instance, type, props, finishedWork);
|
|
}
|
|
|
|
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 onRender = finishedWork.memoizedProps.onRender;
|
|
|
|
if (enableSchedulerTracing) {
|
|
onRender(
|
|
finishedWork.memoizedProps.id,
|
|
current === null ? 'mount' : 'update',
|
|
finishedWork.actualDuration,
|
|
finishedWork.treeBaseDuration,
|
|
finishedWork.actualStartTime,
|
|
getCommitTime(),
|
|
finishedRoot.memoizedInteractions,
|
|
);
|
|
} else {
|
|
onRender(
|
|
finishedWork.memoizedProps.id,
|
|
current === null ? 'mount' : 'update',
|
|
finishedWork.actualDuration,
|
|
finishedWork.treeBaseDuration,
|
|
finishedWork.actualStartTime,
|
|
getCommitTime(),
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case SuspenseComponent:
|
|
case IncompleteClassComponent:
|
|
return;
|
|
case EventTarget: {
|
|
if (enableEventAPI) {
|
|
const type = finishedWork.type.type;
|
|
const props = finishedWork.memoizedProps;
|
|
const instance = finishedWork.stateNode;
|
|
let parentInstance = null;
|
|
|
|
let node = finishedWork.return;
|
|
// Traverse up the fiber tree until we find the parent host node.
|
|
while (node !== null) {
|
|
if (node.tag === HostComponent) {
|
|
parentInstance = node.stateNode;
|
|
break;
|
|
} else if (node.tag === HostRoot) {
|
|
parentInstance = node.stateNode.containerInfo;
|
|
break;
|
|
}
|
|
node = node.return;
|
|
}
|
|
invariant(
|
|
parentInstance !== null,
|
|
'This should have a parent host component initialized. This error is likely ' +
|
|
'caused by a bug in React. Please file an issue.',
|
|
);
|
|
commitEventTarget(type, props, instance, parentInstance);
|
|
}
|
|
return;
|
|
}
|
|
case EventComponent: {
|
|
if (enableEventAPI) {
|
|
mountEventComponent(finishedWork.stateNode);
|
|
}
|
|
return;
|
|
}
|
|
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.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function hideOrUnhideAllChildren(finishedWork, isHidden) {
|
|
if (supportsMutation) {
|
|
// We only have the top Fiber that was inserted but we need to recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node: Fiber = finishedWork;
|
|
while (true) {
|
|
if (node.tag === HostComponent) {
|
|
const instance = node.stateNode;
|
|
if (isHidden) {
|
|
hideInstance(instance);
|
|
} else {
|
|
unhideInstance(node.stateNode, node.memoizedProps);
|
|
}
|
|
} else if (node.tag === HostText) {
|
|
const instance = node.stateNode;
|
|
if (isHidden) {
|
|
hideTextInstance(instance);
|
|
} else {
|
|
unhideTextInstance(instance, node.memoizedProps);
|
|
}
|
|
} else if (
|
|
node.tag === SuspenseComponent &&
|
|
node.memoizedState !== null
|
|
) {
|
|
// Found a nested Suspense component that timed out. Skip over the
|
|
// primary child fragment, which should remain hidden.
|
|
const fallbackChildFragment: Fiber = (node.child: any).sibling;
|
|
fallbackChildFragment.return = node;
|
|
node = fallbackChildFragment;
|
|
continue;
|
|
} else if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
if (node === finishedWork) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === finishedWork) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitAttachRef(finishedWork: Fiber) {
|
|
const ref = finishedWork.ref;
|
|
if (ref !== null) {
|
|
const instance = finishedWork.stateNode;
|
|
let instanceToUse;
|
|
switch (finishedWork.tag) {
|
|
case HostComponent:
|
|
instanceToUse = getPublicInstance(instance);
|
|
break;
|
|
default:
|
|
instanceToUse = instance;
|
|
}
|
|
if (typeof ref === 'function') {
|
|
ref(instanceToUse);
|
|
} else {
|
|
if (__DEV__) {
|
|
if (!ref.hasOwnProperty('current')) {
|
|
warningWithoutStack(
|
|
false,
|
|
'Unexpected ref object provided for %s. ' +
|
|
'Use either a ref-setter function or React.createRef().%s',
|
|
getComponentName(finishedWork.type),
|
|
getStackByFiberInDevAndProd(finishedWork),
|
|
);
|
|
}
|
|
}
|
|
|
|
ref.current = instanceToUse;
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitDetachRef(current: Fiber) {
|
|
const currentRef = current.ref;
|
|
if (currentRef !== null) {
|
|
if (typeof currentRef === 'function') {
|
|
currentRef(null);
|
|
} else {
|
|
currentRef.current = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// User-originating errors (lifecycles and refs) should not interrupt
|
|
// deletion, so don't let them throw. Host-originating errors should
|
|
// interrupt deletion, so it's okay
|
|
function commitUnmount(current: Fiber): void {
|
|
onCommitUnmount(current);
|
|
|
|
switch (current.tag) {
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case MemoComponent:
|
|
case SimpleMemoComponent: {
|
|
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
|
|
if (updateQueue !== null) {
|
|
const lastEffect = updateQueue.lastEffect;
|
|
if (lastEffect !== null) {
|
|
const firstEffect = lastEffect.next;
|
|
let effect = firstEffect;
|
|
do {
|
|
const destroy = effect.destroy;
|
|
if (destroy !== undefined) {
|
|
safelyCallDestroy(current, destroy);
|
|
}
|
|
effect = effect.next;
|
|
} while (effect !== firstEffect);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ClassComponent: {
|
|
safelyDetachRef(current);
|
|
const instance = current.stateNode;
|
|
if (typeof instance.componentWillUnmount === 'function') {
|
|
safelyCallComponentWillUnmount(current, instance);
|
|
}
|
|
return;
|
|
}
|
|
case HostComponent: {
|
|
safelyDetachRef(current);
|
|
return;
|
|
}
|
|
case HostPortal: {
|
|
// TODO: this is recursive.
|
|
// We are also not using this parent because
|
|
// the portal will get pushed immediately.
|
|
if (supportsMutation) {
|
|
unmountHostComponents(current);
|
|
} else if (supportsPersistence) {
|
|
emptyPortalContainer(current);
|
|
}
|
|
return;
|
|
}
|
|
case EventComponent: {
|
|
if (enableEventAPI) {
|
|
const eventComponentInstance = current.stateNode;
|
|
unmountEventComponent(eventComponentInstance);
|
|
current.stateNode = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitNestedUnmounts(root: Fiber): void {
|
|
// While we're inside a removed host node we don't want to call
|
|
// removeChild on the inner nodes because they're removed by the top
|
|
// call anyway. We also want to call componentWillUnmount on all
|
|
// composites before this host node is removed from the tree. Therefore
|
|
// we do an inner loop while we're still inside the host node.
|
|
let node: Fiber = root;
|
|
while (true) {
|
|
commitUnmount(node);
|
|
// Visit children because they may contain more composite or host nodes.
|
|
// Skip portals because commitUnmount() currently visits them recursively.
|
|
if (
|
|
node.child !== null &&
|
|
// If we use mutation we drill down into portals using commitUnmount above.
|
|
// If we don't use mutation we drill down into portals here instead.
|
|
(!supportsMutation || node.tag !== HostPortal)
|
|
) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
if (node === root) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === root) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
}
|
|
|
|
function detachFiber(current: 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
|
|
// 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.
|
|
current.return = null;
|
|
current.child = null;
|
|
current.memoizedState = null;
|
|
current.updateQueue = null;
|
|
const alternate = current.alternate;
|
|
if (alternate !== null) {
|
|
alternate.return = null;
|
|
alternate.child = null;
|
|
alternate.memoizedState = null;
|
|
alternate.updateQueue = null;
|
|
}
|
|
}
|
|
|
|
function emptyPortalContainer(current: Fiber) {
|
|
if (!supportsPersistence) {
|
|
return;
|
|
}
|
|
|
|
const portal: {containerInfo: Container, pendingChildren: ChildSet} =
|
|
current.stateNode;
|
|
const {containerInfo} = portal;
|
|
const emptyChildSet = createContainerChildSet(containerInfo);
|
|
replaceContainerChildren(containerInfo, emptyChildSet);
|
|
}
|
|
|
|
function commitContainer(finishedWork: Fiber) {
|
|
if (!supportsPersistence) {
|
|
return;
|
|
}
|
|
|
|
switch (finishedWork.tag) {
|
|
case ClassComponent:
|
|
case HostComponent:
|
|
case HostText:
|
|
case EventTarget:
|
|
case EventComponent: {
|
|
return;
|
|
}
|
|
case HostRoot:
|
|
case HostPortal: {
|
|
const portalOrRoot: {
|
|
containerInfo: Container,
|
|
pendingChildren: ChildSet,
|
|
} =
|
|
finishedWork.stateNode;
|
|
const {containerInfo, pendingChildren} = portalOrRoot;
|
|
replaceContainerChildren(containerInfo, pendingChildren);
|
|
return;
|
|
}
|
|
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.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getHostParentFiber(fiber: Fiber): Fiber {
|
|
let parent = fiber.return;
|
|
while (parent !== null) {
|
|
if (isHostParent(parent)) {
|
|
return parent;
|
|
}
|
|
parent = parent.return;
|
|
}
|
|
invariant(
|
|
false,
|
|
'Expected to find a host parent. This error is likely caused by a bug ' +
|
|
'in React. Please file an issue.',
|
|
);
|
|
}
|
|
|
|
function isHostParent(fiber: Fiber): boolean {
|
|
return (
|
|
fiber.tag === HostComponent ||
|
|
fiber.tag === HostRoot ||
|
|
fiber.tag === HostPortal
|
|
);
|
|
}
|
|
|
|
function getHostSibling(fiber: Fiber): ?Instance {
|
|
// We're going to search forward into the tree until we find a sibling host
|
|
// node. Unfortunately, if multiple insertions are done in a row we have to
|
|
// search past them. This leads to exponential search for the next sibling.
|
|
// TODO: Find a more efficient way to do this.
|
|
let node: Fiber = fiber;
|
|
siblings: while (true) {
|
|
// If we didn't find anything, let's try the next sibling.
|
|
while (node.sibling === null) {
|
|
if (node.return === null || isHostParent(node.return)) {
|
|
// If we pop out of the root or hit the parent the fiber we are the
|
|
// last sibling.
|
|
return null;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
while (
|
|
node.tag !== HostComponent &&
|
|
node.tag !== HostText &&
|
|
node.tag !== DehydratedSuspenseComponent
|
|
) {
|
|
// If it is not host node and, we might have a host node inside it.
|
|
// Try to search down until we find one.
|
|
if (node.effectTag & Placement) {
|
|
// If we don't have a child, try the siblings instead.
|
|
continue siblings;
|
|
}
|
|
// If we don't have a child, try the siblings instead.
|
|
// We also skip portals because they are not part of this host tree.
|
|
if (node.child === null || node.tag === HostPortal) {
|
|
continue siblings;
|
|
} else {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
}
|
|
}
|
|
// Check if this host node is stable or about to be placed.
|
|
if (!(node.effectTag & Placement)) {
|
|
// Found it!
|
|
return node.stateNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitPlacement(finishedWork: Fiber): void {
|
|
if (!supportsMutation) {
|
|
return;
|
|
}
|
|
|
|
// Recursively insert all host nodes into the parent.
|
|
const parentFiber = getHostParentFiber(finishedWork);
|
|
|
|
// Note: these two variables *must* always be updated together.
|
|
let parent;
|
|
let isContainer;
|
|
|
|
switch (parentFiber.tag) {
|
|
case HostComponent:
|
|
parent = parentFiber.stateNode;
|
|
isContainer = false;
|
|
break;
|
|
case HostRoot:
|
|
parent = parentFiber.stateNode.containerInfo;
|
|
isContainer = true;
|
|
break;
|
|
case HostPortal:
|
|
parent = parentFiber.stateNode.containerInfo;
|
|
isContainer = true;
|
|
break;
|
|
default:
|
|
invariant(
|
|
false,
|
|
'Invalid host parent fiber. This error is likely caused by a bug ' +
|
|
'in React. Please file an issue.',
|
|
);
|
|
}
|
|
if (parentFiber.effectTag & ContentReset) {
|
|
// Reset the text content of the parent before doing any insertions
|
|
resetTextContent(parent);
|
|
// Clear ContentReset from the effect tag
|
|
parentFiber.effectTag &= ~ContentReset;
|
|
}
|
|
|
|
const before = getHostSibling(finishedWork);
|
|
// We only have the top Fiber that was inserted but we need to recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node: Fiber = finishedWork;
|
|
while (true) {
|
|
if (node.tag === HostComponent || node.tag === HostText) {
|
|
const stateNode = node.stateNode;
|
|
if (before) {
|
|
if (isContainer) {
|
|
insertInContainerBefore(parent, stateNode, before);
|
|
} else {
|
|
insertBefore(parent, stateNode, before);
|
|
}
|
|
} else {
|
|
if (isContainer) {
|
|
appendChildToContainer(parent, stateNode);
|
|
} else {
|
|
appendChild(parent, stateNode);
|
|
}
|
|
}
|
|
} else if (node.tag === HostPortal) {
|
|
// If the insertion itself is a portal, then we don't want to traverse
|
|
// down its children. Instead, we'll get insertions from each child in
|
|
// the portal directly.
|
|
} else if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
if (node === finishedWork) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === finishedWork) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
}
|
|
|
|
function unmountHostComponents(current): void {
|
|
// We only have the top Fiber that was deleted but we need to recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node: Fiber = current;
|
|
|
|
// Each iteration, currentParent is populated with node's host parent if not
|
|
// currentParentIsValid.
|
|
let currentParentIsValid = false;
|
|
|
|
// Note: these two variables *must* always be updated together.
|
|
let currentParent;
|
|
let currentParentIsContainer;
|
|
|
|
while (true) {
|
|
if (!currentParentIsValid) {
|
|
let parent = node.return;
|
|
findParent: while (true) {
|
|
invariant(
|
|
parent !== null,
|
|
'Expected to find a host parent. This error is likely caused by ' +
|
|
'a bug in React. Please file an issue.',
|
|
);
|
|
switch (parent.tag) {
|
|
case HostComponent:
|
|
currentParent = parent.stateNode;
|
|
currentParentIsContainer = false;
|
|
break findParent;
|
|
case HostRoot:
|
|
currentParent = parent.stateNode.containerInfo;
|
|
currentParentIsContainer = true;
|
|
break findParent;
|
|
case HostPortal:
|
|
currentParent = parent.stateNode.containerInfo;
|
|
currentParentIsContainer = true;
|
|
break findParent;
|
|
}
|
|
parent = parent.return;
|
|
}
|
|
currentParentIsValid = true;
|
|
}
|
|
|
|
if (node.tag === HostComponent || node.tag === HostText) {
|
|
commitNestedUnmounts(node);
|
|
// After all the children have unmounted, it is now safe to remove the
|
|
// node from the tree.
|
|
if (currentParentIsContainer) {
|
|
removeChildFromContainer(
|
|
((currentParent: any): Container),
|
|
(node.stateNode: Instance | TextInstance),
|
|
);
|
|
} else {
|
|
removeChild(
|
|
((currentParent: any): Instance),
|
|
(node.stateNode: Instance | TextInstance),
|
|
);
|
|
}
|
|
// Don't visit children because we already visited them.
|
|
} else if (
|
|
enableSuspenseServerRenderer &&
|
|
node.tag === DehydratedSuspenseComponent
|
|
) {
|
|
// Delete the dehydrated suspense boundary and all of its content.
|
|
if (currentParentIsContainer) {
|
|
clearSuspenseBoundaryFromContainer(
|
|
((currentParent: any): Container),
|
|
(node.stateNode: SuspenseInstance),
|
|
);
|
|
} else {
|
|
clearSuspenseBoundary(
|
|
((currentParent: any): Instance),
|
|
(node.stateNode: SuspenseInstance),
|
|
);
|
|
}
|
|
} else if (node.tag === HostPortal) {
|
|
if (node.child !== null) {
|
|
// When we go into a portal, it becomes the parent to remove from.
|
|
// We will reassign it back when we pop the portal on the way up.
|
|
currentParent = node.stateNode.containerInfo;
|
|
currentParentIsContainer = true;
|
|
// Visit children because portals might contain host components.
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
} else {
|
|
commitUnmount(node);
|
|
// Visit children because we may find more host components below.
|
|
if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
}
|
|
if (node === current) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === current) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
if (node.tag === HostPortal) {
|
|
// When we go out of the portal, we need to restore the parent.
|
|
// Since we don't keep a stack of them, we will search for it.
|
|
currentParentIsValid = false;
|
|
}
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
}
|
|
|
|
function commitDeletion(current: Fiber): void {
|
|
if (supportsMutation) {
|
|
// Recursively delete all host nodes from the parent.
|
|
// Detach refs and call componentWillUnmount() on the whole subtree.
|
|
unmountHostComponents(current);
|
|
} else {
|
|
// Detach refs and call componentWillUnmount() on the whole subtree.
|
|
commitNestedUnmounts(current);
|
|
}
|
|
detachFiber(current);
|
|
}
|
|
|
|
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
|
|
if (!supportsMutation) {
|
|
switch (finishedWork.tag) {
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case MemoComponent:
|
|
case SimpleMemoComponent: {
|
|
// Note: We currently never use MountMutation, but useLayout uses
|
|
// UnmountMutation.
|
|
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
|
|
return;
|
|
}
|
|
case Profiler: {
|
|
return;
|
|
}
|
|
case SuspenseComponent: {
|
|
commitSuspenseComponent(finishedWork);
|
|
return;
|
|
}
|
|
}
|
|
|
|
commitContainer(finishedWork);
|
|
return;
|
|
}
|
|
|
|
switch (finishedWork.tag) {
|
|
case FunctionComponent:
|
|
case ForwardRef:
|
|
case MemoComponent:
|
|
case SimpleMemoComponent: {
|
|
// Note: We currently never use MountMutation, but useLayout uses
|
|
// UnmountMutation.
|
|
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
|
|
return;
|
|
}
|
|
case ClassComponent: {
|
|
return;
|
|
}
|
|
case HostComponent: {
|
|
const instance: Instance = finishedWork.stateNode;
|
|
if (instance != null) {
|
|
// Commit the work prepared earlier.
|
|
const newProps = finishedWork.memoizedProps;
|
|
// For hydration we reuse the update path but we treat the oldProps
|
|
// as the newProps. The updatePayload will contain the real change in
|
|
// this case.
|
|
const oldProps = current !== null ? current.memoizedProps : newProps;
|
|
const type = finishedWork.type;
|
|
// TODO: Type the updateQueue to be specific to host components.
|
|
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
|
|
finishedWork.updateQueue = null;
|
|
if (updatePayload !== null) {
|
|
commitUpdate(
|
|
instance,
|
|
updatePayload,
|
|
type,
|
|
oldProps,
|
|
newProps,
|
|
finishedWork,
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case HostText: {
|
|
invariant(
|
|
finishedWork.stateNode !== null,
|
|
'This should have a text node initialized. This error is likely ' +
|
|
'caused by a bug in React. Please file an issue.',
|
|
);
|
|
const textInstance: TextInstance = finishedWork.stateNode;
|
|
const newText: string = finishedWork.memoizedProps;
|
|
// For hydration we reuse the update path but we treat the oldProps
|
|
// as the newProps. The updatePayload will contain the real change in
|
|
// this case.
|
|
const oldText: string =
|
|
current !== null ? current.memoizedProps : newText;
|
|
commitTextUpdate(textInstance, oldText, newText);
|
|
return;
|
|
}
|
|
case EventTarget: {
|
|
return;
|
|
}
|
|
case HostRoot: {
|
|
return;
|
|
}
|
|
case Profiler: {
|
|
return;
|
|
}
|
|
case SuspenseComponent: {
|
|
commitSuspenseComponent(finishedWork);
|
|
return;
|
|
}
|
|
case IncompleteClassComponent: {
|
|
return;
|
|
}
|
|
case EventComponent: {
|
|
return;
|
|
}
|
|
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.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function commitSuspenseComponent(finishedWork: Fiber) {
|
|
let newState: SuspenseState | null = finishedWork.memoizedState;
|
|
|
|
let newDidTimeout;
|
|
let primaryChildParent = finishedWork;
|
|
if (newState === null) {
|
|
newDidTimeout = false;
|
|
} else {
|
|
newDidTimeout = true;
|
|
primaryChildParent = finishedWork.child;
|
|
markCommitTimeOfFallback();
|
|
}
|
|
|
|
if (supportsMutation && primaryChildParent !== null) {
|
|
hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
|
|
}
|
|
|
|
// If this boundary just timed out, then it will have a set of thenables.
|
|
// For each thenable, attach a listener so that when it resolves, React
|
|
// attempts to re-render the boundary in the primary (pre-timeout) state.
|
|
const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
|
|
if (thenables !== null) {
|
|
finishedWork.updateQueue = null;
|
|
let retryCache = finishedWork.stateNode;
|
|
if (retryCache === null) {
|
|
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
|
|
}
|
|
thenables.forEach(thenable => {
|
|
// Memoize using the boundary fiber to prevent redundant listeners.
|
|
let retry = resolveRetryThenable.bind(null, finishedWork, thenable);
|
|
if (!retryCache.has(thenable)) {
|
|
if (enableSchedulerTracing) {
|
|
retry = Schedule_tracing_wrap(retry);
|
|
}
|
|
retryCache.add(thenable);
|
|
thenable.then(retry, retry);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function commitResetTextContent(current: Fiber) {
|
|
if (!supportsMutation) {
|
|
return;
|
|
}
|
|
resetTextContent(current.stateNode);
|
|
}
|
|
|
|
export {
|
|
commitBeforeMutationLifeCycles,
|
|
commitResetTextContent,
|
|
commitPlacement,
|
|
commitDeletion,
|
|
commitWork,
|
|
commitLifeCycles,
|
|
commitAttachRef,
|
|
commitDetachRef,
|
|
};
|