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

  1. The first execution will callmountSuspensePrimaryChildren
  2. mountSuspensePrimaryChildrenInternally calledmountWorkInProgressOffscreenFiberTo generate the$$typeofSymbol(react.offscreen)Type fiber node. The lazy component waits for the next reconciliation as children in the pendingProps of the Fiber node.
  3. $$typeofSymbol(react.offscreen)Type of fiber node callreconcileChildFibersGenerate 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: