Files
react/packages/react-reconciler/src/ReactFiberUnwindWork.js
T
Andrew Clark b548b3cd64 Decouple update queue from Fiber type (#12600)
* Decouple update queue from Fiber type

The update queue is in need of a refactor. Recent bugfixes (#12528) have
exposed some flaws in how it's modeled. Upcoming features like Suspense
and [redacted] also rely on the update queue in ways that weren't
anticipated in the original design.

Major changes:

- Instead of boolean flags for `isReplace` and `isForceUpdate`, updates
have a `tag` field (like Fiber). This lowers the cost for adding new
types of updates.
- Render phase updates are special cased. Updates scheduled during
the render phase are dropped if the work-in-progress does not commit.
This is used for `getDerivedStateFrom{Props,Catch}`.
- `callbackList` has been replaced with a generic effect list. Aside
from callbacks, this is also used for `componentDidCatch`.

* Remove first class UpdateQueue types and use closures instead

I tried to avoid this at first, since we avoid it everywhere else in the Fiber
codebase, but since updates are not in a hot path, the trade off with file size
seems worth it.

* Store captured errors on a separate part of the update queue

This way they can be reused independently of updates like
getDerivedStateFromProps. This will be important for resuming.

* Revert back to storing hasForceUpdate on the update queue

Instead of using the effect tag. Ideally, this would be part of the
return type of processUpdateQueue.

* Rename UpdateQueue effect type back to Callback

I don't love this name either, but it's less confusing than UpdateQueue
I suppose. Conceptually, this is usually a callback: setState callbacks,
componentDidCatch. The only case that feels a bit weird is Timeouts,
which use this effect to attach a promise listener. I guess that kinda
fits, too.

* Call getDerivedStateFromProps every render, even if props did not change

Rather than enqueue a new setState updater for every props change, we
can skip the update queue entirely and merge the result into state at
the end. This makes more sense, since "receiving props" is not an event
that should be observed. It's still a bit weird, since eventually we do
persist the derived state (in other words, it accumulates).

* Store captured effects on separate list from "own" effects (callbacks)

For resuming, we need the ability to discard the "own" effects while
reusing the captured effects.

* Optimize for class components

Change `process` and `callback` to match the expected payload types
for class components. I had intended for the update queue to be reusable
for both class components and a future React API, but we'll likely have
to fork anyway.

* Only double-invoke render phase lifecycles functions in DEV

* Use global state to track currently processing queue in DEV
2018-04-22 23:05:28 -07:00

250 lines
7.3 KiB
JavaScript

/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* 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 {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {HostContext} from './ReactFiberHostContext';
import type {LegacyContext} from './ReactFiberContext';
import type {NewContext} from './ReactFiberNewContext';
import type {CapturedValue} from './ReactCapturedValue';
import type {Update} from './ReactUpdateQueue';
import {createCapturedValue} from './ReactCapturedValue';
import {
enqueueCapturedUpdate,
createUpdate,
CaptureUpdate,
} from './ReactUpdateQueue';
import {logError} from './ReactFiberCommitWork';
import {
ClassComponent,
HostRoot,
HostComponent,
HostPortal,
ContextProvider,
} from 'shared/ReactTypeOfWork';
import {
NoEffect,
DidCapture,
Incomplete,
ShouldCapture,
} from 'shared/ReactTypeOfSideEffect';
import {enableGetDerivedStateFromCatch} from 'shared/ReactFeatureFlags';
export default function<C, CX>(
hostContext: HostContext<C, CX>,
legacyContext: LegacyContext,
newContext: NewContext,
scheduleWork: (
fiber: Fiber,
startTime: ExpirationTime,
expirationTime: ExpirationTime,
) => void,
markLegacyErrorBoundaryAsFailed: (instance: mixed) => void,
isAlreadyFailedLegacyErrorBoundary: (instance: mixed) => boolean,
onUncaughtError: (error: mixed) => void,
) {
const {popHostContainer, popHostContext} = hostContext;
const {
popContextProvider: popLegacyContextProvider,
popTopLevelContextObject: popTopLevelLegacyContextObject,
} = legacyContext;
const {popProvider} = newContext;
function createRootErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<null> {
const update = createUpdate(expirationTime);
// Unmount the root by rendering null.
update.tag = CaptureUpdate;
update.payload = {children: null};
const error = errorInfo.value;
update.callback = () => {
onUncaughtError(error);
logError(fiber, errorInfo);
};
return update;
}
function createClassErrorUpdate(
fiber: Fiber,
errorInfo: CapturedValue<mixed>,
expirationTime: ExpirationTime,
): Update<mixed> {
const update = createUpdate(expirationTime);
update.tag = CaptureUpdate;
const getDerivedStateFromCatch = fiber.type.getDerivedStateFromCatch;
if (
enableGetDerivedStateFromCatch &&
typeof getDerivedStateFromCatch === 'function'
) {
const error = errorInfo.value;
update.payload = () => {
return getDerivedStateFromCatch(error);
};
}
const inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === 'function') {
update.callback = function callback() {
if (
!enableGetDerivedStateFromCatch ||
getDerivedStateFromCatch !== 'function'
) {
// To preserve the preexisting retry behavior of error boundaries,
// we keep track of which ones already failed during this batch.
// This gets reset before we yield back to the browser.
// TODO: Warn in strict mode if getDerivedStateFromCatch is
// not defined.
markLegacyErrorBoundaryAsFailed(this);
}
const error = errorInfo.value;
const stack = errorInfo.stack;
logError(fiber, errorInfo);
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : '',
});
};
}
return update;
}
function throwException(
returnFiber: Fiber,
sourceFiber: Fiber,
rawValue: mixed,
renderExpirationTime: ExpirationTime,
) {
// The source fiber did not complete.
sourceFiber.effectTag |= Incomplete;
// Its effect list is no longer valid.
sourceFiber.firstEffect = sourceFiber.lastEffect = null;
const value = createCapturedValue(rawValue, sourceFiber);
let workInProgress = returnFiber;
do {
switch (workInProgress.tag) {
case HostRoot: {
const errorInfo = value;
workInProgress.effectTag |= ShouldCapture;
const update = createRootErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
return;
}
case ClassComponent:
// Capture and retry
const errorInfo = value;
const ctor = workInProgress.type;
const instance = workInProgress.stateNode;
if (
(workInProgress.effectTag & DidCapture) === NoEffect &&
((typeof ctor.getDerivedStateFromCatch === 'function' &&
enableGetDerivedStateFromCatch) ||
(instance !== null &&
typeof instance.componentDidCatch === 'function' &&
!isAlreadyFailedLegacyErrorBoundary(instance)))
) {
workInProgress.effectTag |= ShouldCapture;
// Schedule the error boundary to re-render using updated state
const update = createClassErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime,
);
enqueueCapturedUpdate(workInProgress, update, renderExpirationTime);
return;
}
break;
default:
break;
}
workInProgress = workInProgress.return;
} while (workInProgress !== null);
}
function unwindWork(workInProgress: Fiber) {
switch (workInProgress.tag) {
case ClassComponent: {
popLegacyContextProvider(workInProgress);
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
case HostComponent: {
popHostContext(workInProgress);
return null;
}
case HostPortal:
popHostContainer(workInProgress);
return null;
case ContextProvider:
popProvider(workInProgress);
return null;
default:
return null;
}
}
function unwindInterruptedWork(interruptedWork: Fiber) {
switch (interruptedWork.tag) {
case ClassComponent: {
popLegacyContextProvider(interruptedWork);
break;
}
case HostRoot: {
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
break;
}
case HostComponent: {
popHostContext(interruptedWork);
break;
}
case HostPortal:
popHostContainer(interruptedWork);
break;
case ContextProvider:
popProvider(interruptedWork);
break;
default:
break;
}
}
return {
throwException,
unwindWork,
unwindInterruptedWork,
createRootErrorUpdate,
createClassErrorUpdate,
};
}