The code structure
Source code based on React 17.0.3
Suppose our code has the following structure:
function MyComponent() {
return (
// Displays <Spinner> until OtherComponent loads
<React.Suspense fallback={<Spinner />}>
{React.lazy(() => import('./OtherComponent'))}
</React.Suspense>
);
}
Copy the code
The main process
First call react. CreateElement to generate a special structure with a $$Typeof property in it. If the attribute is Symbol(React.suspense), updateSuspenseComponent is called to generate the corresponding fiber node.
packages/react-reconciler/src/ReactFiberBeginWork.old.js
function updateSuspenseComponent(current, workInProgress, renderLanes) {
const nextProps = workInProgress.pendingProps;
let suspenseContext: SuspenseContext = suspenseStackCursor.current;
let showFallback = false;
// Execute the value false the first time to build the lazy component
// Execute again after the lazy component throws error, which is true
constdidSuspend = (workInProgress.flags & DidCapture) ! == NoFlags;if (
didSuspend ||
shouldRemainOnFallback(
suspenseContext,
current,
workInProgress,
renderLanes,
)
) {
// Something in this boundary's subtree already suspended. Switch to
// rendering the fallback children.
showFallback = true;
workInProgress.flags &= ~DidCapture;
} else {
// Attempting the main content
if (
current === null ||
(current.memoizedState: null| SuspenseState) ! = =null
) {
// This is a new mount or this boundary is already showing a fallback state.
// Mark this subtree context as having at least one invisible parent that could
// handle the fallback state.
// Avoided boundaries are not considered since they cannot handle preferred fallback states.
if(nextProps.unstable_avoidThisFallback ! = =true) {
suspenseContext = addSubtreeSuspenseContext(
suspenseContext,
InvisibleParentSuspenseContext,
);
}
}
}
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
pushSuspenseContext(workInProgress, suspenseContext);
if (current === null) {
// Initial mount
// If we're currently hydrating, try to hydrate this boundary.
tryToClaimNextHydratableInstance(workInProgress);
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
if (showFallback) {
// Enter here for the second execution
const fallbackFragment = mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackFragment;
} else {
// Enter here for the first time
returnmountSuspensePrimaryChildren( workInProgress, nextPrimaryChildren, renderLanes, ); }}else {
// This is an update.. }}}Copy the code
Here’s an overview of the updateSuspenseComponent process
- The first execution will call
mountSuspensePrimaryChildren
mountSuspensePrimaryChildren
Internally calledmountWorkInProgressOffscreenFiber
To generate the$$typeof
为Symbol(react.offscreen)
Type fiber node. The lazy component waits for the next reconciliation as children in the pendingProps of the Fiber node.$$typeof
为Symbol(react.offscreen)
Type of fiber node callreconcileChildFibers
Generate sub-fiber nodes for lazy components based on pendingProps.
For reconcileChildFibers, the source code is as follows:
packages/react-reconciler/src/ReactChildFiber.old.js
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
) :Fiber | null {...// newChild has the following structure
/** {$$typeof: Symbol(react.lazy) _init: ƒ lazyInitializer(payload) _payload: {_status: -1, _result: ƒ}} */
// Handle object types
if (typeof newChild === 'object'&& newChild ! = =null) {
switch (newChild.$$typeof) {
case REACT_LAZY_TYPE:
if (enableLazyElements) {
const payload = newChild._payload;
const init = newChild._init;
// TODO: This function is supposed to be non-recursive.
returnreconcileChildFibers( returnFiber, currentFirstChild, init(payload), lanes, ); }}}... }Copy the code
The init method in the lazy component is called as follows:
packages/react/src/ReactLazy.js
function lazyInitializer<T> (payload: Payload<T>) :T {
if (payload._status === Uninitialized) {
const ctor = payload._result;
const thenable = ctor();
// Transition to the next state.
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
thenable.then(
(moduleObject) = > {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
constresolved: ResolvedPayload<T> = (payload: any); resolved._status = Resolved; resolved._result = moduleObject; }},(error) = > {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
constrejected: RejectedPayload = (payload: any); rejected._status = Rejected; rejected._result = error; }});if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
constpending: PendingPayload = (payload: any); pending._status = Pending; pending._result = thenable; }}if (payload._status === Resolved) {
const moduleObject = payload._result;
return moduleObject.default;
} else {
throwpayload._result; }}export function lazy<T> (
ctor: () => Thenable<{default: T, ... } >.) :LazyComponent<T.Payload<T>> {
const payload: Payload<T> = {
// We use these fields to store the result.
_status: -1._result: ctor,
};
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,
};
return lazyType;
}
Copy the code
When init is called, paypay. _status is -1, and paypay. _result, the promise function passed in by the lazy component, is thrown. Let’s look at how React handles errors.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function renderRootSync(root: FiberRoot, lanes: Lanes) {...do {
try {
workLoopSync();
break;
} catch(thrownValue) { handleError(root, thrownValue); }}while (true); . }function handleError(root, thrownValue) :void {
do{...let erroredWork = workInProgress;
try {
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
...
continue;
}
// Return to the normal work loop.
return;
} while (true);
}
Copy the code
packages/react-reconciler/src/ReactFiberThrow.old.js
function throwException(root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, rootRenderLanes: Lanes,) {
// The source fiber did not complete.
sourceFiber.flags |= Incomplete;
// This is a wakeable.
const wakeable: Wakeable = (value: any);
// Reset the memoizedState to what it was before we attempted to render it.
// A legacy mode Suspense quirk, only relevant to hook components.
const tag = sourceFiber.tag;
// Schedule the nearest Suspense to re-render the timed out view.
let workInProgress = returnFiber;
do {
if (
workInProgress.tag === SuspenseComponent &&
shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
) {
// Found the nearest boundary.
// Stash the promise on the boundary fiber. If the boundary times out, we'll
// attach another listener to flip the boundary back to its normal state.
const wakeables: Set<Wakeable> = (workInProgress.updateQueue: any);
if (wakeables === null) {
const updateQueue = (new Set(): any);
// Attach the promise to updateQueues in workInProgress
updateQueue.add(wakeable);
workInProgress.updateQueue = updateQueue;
} else {
wakeables.add(wakeable);
}
// Implement a listener to ensure that the page is updated after the lazy component loading completes
attachPingListener(root, wakeable, rootRenderLanes);
workInProgress.flags |= ShouldCapture;
// TODO: I think we can remove this, since we now use `DidCapture` in
// the begin phase to prevent an early bailout.
workInProgress.lanes = rootRenderLanes;
return;
}
// This boundary already captured during this render. Continue to the next
// boundary.
workInProgress = workInProgress.return;
} while(workInProgress ! = =null); }... }function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
let pingCache = root.pingCache;
let threadIDs;
if (pingCache === null) {
pingCache = root.pingCache = new PossiblyWeakMap();
threadIDs = new Set(a); pingCache.set(wakeable, threadIDs); }else {
threadIDs = pingCache.get(wakeable);
if (threadIDs === undefined) {
threadIDs = new Set();
pingCache.set(wakeable, threadIDs);
}
}
if(! threadIDs.has(lanes)) {// Memoize using the thread ID to prevent redundant listeners.
threadIDs.add(lanes);
const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// If we have pending work still, restore the original updatersrestorePendingUpdaters(root, lanes); }}// Implement the listener herewakeable.then(ping, ping); }}Copy the code
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function pingSuspendedRoot(root: FiberRoot, wakeable: Wakeable, pingedLanes: Lanes,) {
const pingCache = root.pingCache;
if(pingCache ! = =null) {
// The wakeable resolved, so we no longer need to memoize, because it will
// never be thrown again.
pingCache.delete(wakeable);
}
const eventTime = requestEventTime();
markRootPinged(root, pingedLanes, eventTime);
// The debugger found that the if logic was not used
if (
workInProgressRoot === root &&
isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
) {
// Received a ping at the same priority level at which we're currently
// rendering. We might want to restart this render. This should mirror
// the logic of whether or not a root suspends once it completes.
// TODO: If we're rendering sync either due to Sync, Batched or expired,
// we should probably never restart.
// If we're suspended with delay, or if it's a retry, we'll always suspend
// so we can always restart.
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
// Restart from the root.
prepareFreshStack(root, NoLanes);
} else {
// Even though we can't restart right now, we might get an
// opportunity later. So we mark this render as having a ping.workInProgressRootPingedLanes = mergeLanes( workInProgressRootPingedLanes, pingedLanes, ); }}// Restart the rendering process
ensureRootIsScheduled(root, eventTime);
}
Copy the code
conclusion
Directly above: