Skip to content

Suspense

在 16.6 版本之前,code-spliting 通常是由第三方库来完成的,比如 react-loadble(核心思路为: 高阶组件 + webpack dynamic import), 在 16.6 版本中提供了 Suspenselazy 这两个钩子, 因此在之后的版本中便可以使用其来实现 Code Spliting

作用

  1. 资源预加载

可以配合 React.lazy 使用,提前加载组件资源。

  1. 异步数据处理

Concurrent 模式中,Suspense 能够在数据获取时显示回退(fallback)内容,等数据加载完成后再渲染组件。

例子

jsx
import { Suspense, lazy } from 'react'

const DemoA = lazy(() => import('./demo/a'))
const DemoB = lazy(() => import('./demo/b'))

<Suspense>
  <NavLink to="/demoA">DemoA</NavLink>
  <NavLink to="/demoB">DemoB</NavLink>

  <Router>
    <DemoA path="/demoA" />
    <DemoB path="/demoB" />
  </Router>
</Suspense>

源码解析

对于 Lazy 组件的渲染流程,其仍然是在 beginWork 阶段进行的, 通过 case : SuspenseComponent 判断当前组件为 Suspense 组件,然后进入其 updateSuspenseComponent流程。

Primary 初始化渲染

js
function updateSuspenseComponent(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;

  // 获取当前的 Suspense 上下文
  let suspenseContext: SuspenseContext = suspenseStackCursor.current;
  // 是否需要显示 fallback (加载中状态)
  let showFallback = false;
  // 在 Lazy 组件未加载完成的情况下,其通过throw 异常的方式进入 handleError 流程
  // 通过 throwException 找到当前组件 并标记 ShouldCapture
  // 通过 completeUnitOfWork 阶段的 unwindWork 流程 向上冒泡 将 ShouldCapture 转换成 DidCapture
  const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
  // 需要进入加载中状态

  // 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 (
      !enableSuspenseAvoidThisFallback ||
      nextProps.unstable_avoidThisFallback !== true
    ) {
      suspenseContext = addSubtreeSuspenseContext(
        suspenseContext,
        InvisibleParentSuspenseContext
      );
    }
  }

  // 将新的 Suspense 设置到上下文中
  suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
  // 将新的上下文压栈
  pushSuspenseContext(workInProgress, suspenseContext);

  if (current === null) {
    // Initial mount
    // This could've been a dehydrated suspense component.
    const suspenseState: null | SuspenseState = workInProgress.memoizedState;
    if (suspenseState !== null) {
      const dehydrated = suspenseState.dehydrated;
      if (dehydrated !== null) {
        return mountDehydratedSuspenseComponent(
          workInProgress,
          dehydrated,
          renderLanes
        );
      }
    }
    // 动态加载的子节点
    const nextPrimaryChildren = nextProps.children;
    // 动态加载子节点加载期间显示的内容
    const nextFallbackChildren = nextProps.fallback;
    // 如果

    // 第一次 去初始化执行动态加载子节点
    return mountSuspensePrimaryChildren(
      workInProgress,
      nextPrimaryChildren,
      renderLanes
    );
  }
}

从源码可以看出,在初次渲染的时候,其核心流程如下:

  1. 初始化 Suspense 上下文

通过 suspenseStackCursor.current 缓存当前的 Suspense 上下文中的类型,其具体类型有如下几种:

  • DefaultSuspenseContext: SuspenseContext = 0b00;

默认的 Suspense 上下文状态,通常用于表示当前没有任何 Suspense 边界。

  • SubtreeSuspenseContextMask: SuspenseContext = 0b01;

当前 Fiber 子树的 Suspense 上下文状态。 它通常与其他 SuspenseContext 标志(如 InvisibleParentSuspenseContext)配合使用,用于判断父级或子树中是否存在不可见的 Suspense 边界,从而影响 React 的渲染和回退逻辑

  • InvisibleParentSuspenseContext: SubtreeSuspenseContext = 0b01;

标记父级 Suspense 边界当前未显示主内容(即处于 fallback 或未挂载状态)

  • ForceSuspenseFallback: ShallowSuspenseContext = 0b10;

强制触发 Suspense 边界的 fallback(回退 UI)。当该标志被设置时, 即使正常情况下不需要 fallback,React 也会强制渲染 fallback 内容,常用于错误恢复、数据加载等场景

在初始化的时候 通过 setDefaultShallowSuspenseContext() 将当前全局变量 suspenseStackCursor.current 设置为 parentContext & SubtreeSuspenseContextMask 类型。这样其所以得子节点都可以通过 const hasInvisibleParentBoundary = hasSuspenseContext(suspenseStackCursor.current,(InvisibleParentSuspenseContext: SuspenseContext),); 判断是否有不可见的父级 Suspense 边界。

同时通过 pushSuspenseContext(workInProgress, suspenseContext); 将当前的 Suspense 上下文压栈。

  1. mountSuspensePrimaryChildren

通过 mountSuspensePrimaryChildren 去初次渲染 Suspense 的 Primary状态 的子节点.

js
function mountSuspensePrimaryChildren(
  workInProgress,
  primaryChildren,
  renderLanes
) {
  const mode = workInProgress.mode;
  const primaryChildProps: OffscreenProps = {
    mode: "visible",
    children: primaryChildren,
  };
  // 创建一个 Offscreen 组件 添加到 Suspense 组件中
  const primaryChildFragment = mountWorkInProgressOffscreenFiber(
    primaryChildProps,
    mode,
    renderLanes
  );
  // 修改 Offscreen 组件的返回节点为 Suspense 组件
  primaryChildFragment.return = workInProgress;
  workInProgress.child = primaryChildFragment;
  return primaryChildFragment;
}

发现其创建了一个 Offscreen 组件,并且将其返回节点设置为 Suspense 组件。这样 Suspense => PriamaryComponent 的结构就变成了 Suspense => Offscreen => PriamaryComponent 这样的结构了。

image-20250523172800347

那么为什么要这样做呢?

同时在创建 Offscreen 组件的时候,其会执行 mountWorkInProgressOffscreenFiber 流程,并执行 PrimaryComponentbeginWork 流程,从而进入 Lazy 组件的第一次渲染流程。

  • 然后执行 Lazy 组件的第一次 init(payload)中的 Uninitialized 流程,并 throw 异常。 => 进入 handleError 流程。
  • handleError 流程中,会通过 throwException 找到当前组件 并标记 ShouldCapture
  • completeUnitOfWork 阶段的 unwindWork 流程中,会将 ShouldCapture 转换成 DidCapture

这样就完成了 Suspense 组件的初次渲染流程。

重点

  1. SubtreeSuspenseContextMask 类型的上下文存储到 suspenseStackCursor.current 中并压栈,为子节点提供是否有不可见的父级 Suspense 边界的信息。

  2. Suspense => PriamaryComponent 的结构变成 Suspense => Offscreen => PriamaryComponent

  3. 初次执行 Lazy 组件的 init(payload) 流程,并通过异常处理流程(handleError)找到当前组件并标记 DidCapture类型的 flags 副作用。

Fallback 状态

更新渲染阶段,这时候 Lazy 组件是 Pending 状态,所以这时候不会去渲染 PrimaryComponent 组件,而是会去渲染 fallback 组件。

其核心流程如下:

js
function updateSuspenseComponent(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;

  // 获取当前的 Suspense 上下文
  let suspenseContext: SuspenseContext = suspenseStackCursor.current;
  // 是否需要显示 fallback (加载中状态)
  let showFallback = false;
  // 在 Lazy 组件未加载完成的情况下,其通过throw 异常的方式进入 handleError 流程
  // 通过 throwException 找到当前组件 并标记 ShouldCapture
  // 通过 completeUnitOfWork 阶段的 unwindWork 流程 向上冒泡 将 ShouldCapture 转换成 DidCapture
  const didSuspend = (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;
  }

  // 将新的 Suspense 设置到上下文中
  suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
  // 将新的上下文压栈
  pushSuspenseContext(workInProgress, suspenseContext);

  // This is an update.

  // Special path for hydration
  const prevState: null | SuspenseState = current.memoizedState;
  if (prevState !== null) {
    const dehydrated = prevState.dehydrated;
    if (dehydrated !== null) {
      return updateDehydratedSuspenseComponent(
        current,
        workInProgress,
        didSuspend,
        nextProps,
        dehydrated,
        prevState,
        renderLanes
      );
    }
  }
  // 渲染 fallback 流程
  if (showFallback) {
    const nextFallbackChildren = nextProps.fallback;
    const nextPrimaryChildren = nextProps.children;
    // 更新渲染 fallbackComponent
    const fallbackChildFragment = updateSuspenseFallbackChildren(
      current,
      workInProgress,
      nextPrimaryChildren,
      nextFallbackChildren,
      renderLanes
    );
    const primaryChildFragment: Fiber = (workInProgress.child: any);
    const prevOffscreenState: OffscreenState | null = (current.child: any)
      .memoizedState;
    primaryChildFragment.memoizedState =
      prevOffscreenState === null
        ? mountSuspenseOffscreenState(renderLanes)
        : updateSuspenseOffscreenState(prevOffscreenState, renderLanes);
    if (enableTransitionTracing) {
      const currentTransitions = getSuspendedTransitions();
      if (currentTransitions !== null) {
        const primaryChildUpdateQueue: OffscreenQueue = {
          transitions: currentTransitions,
        };
        primaryChildFragment.updateQueue = primaryChildUpdateQueue;
      }
    }
    primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
      current,
      renderLanes
    );
    workInProgress.memoizedState = SUSPENDED_MARKER;
    return fallbackChildFragment;
  }
}
  1. 判断是否显示 fallback 状态。

const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;


在 Lazy 组件未加载完成的情况下,其通过 throw 异常的方式进入 handleError 流程

  • 通过 throwException 找到当前组件 并标记 ShouldCapture
  • 通过 completeUnitOfWork 阶段的 unwindWork 流程 向上冒泡 将 ShouldCapture 转换成 DidCapture
  1. 初始化 Suspense 上下文

同样通过 setDefaultShallowSuspenseContext() 将当前全局变量 suspenseStackCursor.current 设置为 parentContext & SubtreeSuspenseContextMask 类型。这样其所以得子节点都可以通过 const hasInvisibleParentBoundary = hasSuspenseContext(suspenseStackCursor.current,(InvisibleParentSuspenseContext: SuspenseContext),); 判断是否有不可见的父级 Suspense 边界。

同时通过 pushSuspenseContext(workInProgress, suspenseContext); 将当前的 Suspense 上下文压栈。

  1. 初次渲染 或者 更新渲染 FallbackComponent

这里有一个细节,对于 Suspense 组件来说, 其有两种状态: PrimaryComponentFallbackComponent,且这两种状态是可以相互切换的。那么在PrimaryCompponent 或者 FallbackComponent 状态下,子节点是唯一的么?

实际上并不是,Suspense组件将PrimaryComponentFallbackComponent 两个组件都存放在 Suspense 组件下, 并作为兄弟节点,即如下结构:

image-20250523172333600

js
<Suspense>
  {/* PrimaryComponent */}
  <Offscreen>
    <PrimaryComponent></PrimaryComponent>
  </Offscreen>
  {/* 加载中状态节点 */}
  <Fragment>
    <FallbackComponent></FallbackComponent>
  </Fragment>
</Suspense>

所以解释了为什么在初次渲染的时候,需要将 Suspense => PriamaryComponent 的结构变成 Suspense => Offscreen => PriamaryComponent

因为每次渲染的时候,PrimaryComponent 组件都是作为一个子节点去渲染的,那么如果不用 Offscreen组件包裹,那就无法控制其隐藏的状态。具体可以看 Offscreen 组件的源码, 在 mode : 'hidden'的情况下,不会显示任何内容。

核心源码为

js
function mountSuspenseFallbackChildren(
  workInProgress,
  primaryChildren,
  fallbackChildren,
  renderLanes
) {
  const mode = workInProgress.mode;
  const progressedPrimaryFragment: Fiber | null = workInProgress.child;
  // 创建一个 mode = hidden 的 Offscreen 组件
  const primaryChildProps: OffscreenProps = {
    mode: "hidden",
    children: primaryChildren,
  };

  let primaryChildFragment;
  let fallbackChildFragment;

  // 初始化渲染 Offscreen 组件
  primaryChildFragment = mountWorkInProgressOffscreenFiber(
    primaryChildProps,
    mode,
    NoLanes
  );
  // 初始化渲染 Fragment 组件
  fallbackChildFragment = createFiberFromFragment(
    fallbackChildren,
    mode,
    renderLanes,
    null
  );
  // 形成 Suspense 组件 => Offscreen => PrimaryComponent
  //                   => Fragment  => FallbackComponent
  primaryChildFragment.return = workInProgress;
  fallbackChildFragment.return = workInProgress;
  primaryChildFragment.sibling = fallbackChildFragment;
  workInProgress.child = primaryChildFragment;
  return fallbackChildFragment;
}

Primary 完成渲染

触发条件

初次渲染阶段添加的异步执行回调了,那么就出修改 Lazy 组件的状态为加载完成resolved._status = Resolved,并执行回调函数 attachPingListener,当异步回调函数执行完成后,会触发 ping 事件,从而 触发pingSuspendedRoot 流程。

js
export function pingSuspendedRoot(
  root: FiberRoot,
  wakeable: Wakeable,
  pingedLanes: Lanes
) {
  // 找到所有的 Ping
  const pingCache = root.pingCache;
  // 删除当前已经返回的 Promise
  if (pingCache !== null) {
    // The wakeable resolved, so we no longer need to memoize, because it will
    // never be thrown again.
    pingCache.delete(wakeable);
  }
  // 生成一个 Update 时间
  const eventTime = requestEventTime();
  // 将 Promise 挂载的 lanes 添加到 root.suspendedLanes 中
  markRootPinged(root, pingedLanes, eventTime);

  warnIfSuspenseResolutionNotWrappedWithActDEV(root);
  // 如果 异步车道正好在当前渲染的车道中,那么这时候可能执行完成后移除车道,导致无法执行当前异步任务的更新
  //  所以需要重置一下当前执行渲染流程
  if (
    workInProgressRoot === root &&
    isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
  ) {
    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
      );
    }
  }
  // 调和更新
  ensureRootIsScheduled(root, eventTime);
}

执行流程

js
function updateSuspenseComponent(current, workInProgress, renderLanes) {
  // 在 Lazy 组件未加载完成的情况下,其通过throw 异常的方式进入 handleError 流程
  // 通过 throwException 找到当前组件 并标记 ShouldCapture
  // 通过 completeUnitOfWork 阶段的 unwindWork 流程 向上冒泡 将 ShouldCapture 转换成 DidCapture
  const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;
  // 渲染 Primary 子节点
  const nextPrimaryChildren = nextProps.children;
  const primaryChildFragment = updateSuspensePrimaryChildren(
    current,
    workInProgress,
    nextPrimaryChildren,
    renderLanes
  );
  // 清除缓存的 state
  workInProgress.memoizedState = null;
  return primaryChildFragment;
}

其核心判断 const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags为 fasle 的情况下执行 updateSuspensePrimaryChildren, 所以其核心代码如下

js
function updateSuspensePrimaryChildren(
  current,
  workInProgress,
  primaryChildren,
  renderLanes
) {
  const currentPrimaryChildFragment: Fiber = (current.child: any);
  const currentFallbackChildFragment: Fiber | null =
    currentPrimaryChildFragment.sibling;

  // 以 visible 模式去更新 Offscreen 组件
  const primaryChildFragment = updateWorkInProgressOffscreenFiber(
    currentPrimaryChildFragment,
    {
      mode: "visible",
      children: primaryChildren,
    }
  );
  if ((workInProgress.mode & ConcurrentMode) === NoMode) {
    primaryChildFragment.lanes = renderLanes;
  }
  // 修改 Offscreen 组件的返回节点为 Suspense 组件
  primaryChildFragment.return = workInProgress;
  // 将 Fallback 组件从 Suspense 组件中移除
  primaryChildFragment.sibling = null;
  // 如果 Fallback 组件存在 那么执行 Fallback 组件的删除操作 (ChildDeletion 副作用)
  if (currentFallbackChildFragment !== null) {
    // Delete the fallback child fragment
    const deletions = workInProgress.deletions;
    if (deletions === null) {
      // fallback 组件添加到 待删除列表中
      workInProgress.deletions = [currentFallbackChildFragment];
      // 添加子节点移除副作用
      workInProgress.flags |= ChildDeletion;
    } else {
      deletions.push(currentFallbackChildFragment);
    }
  }
  workInProgress.child = primaryChildFragment;
  return primaryChildFragment;
}

此流程是 Suspense 组件下异步组件加载完成的时候触发的,从而去更新 Suspense 组件的 Primary 子节点,主要流程如下:

  1. 以 visible 模式去更新 Offscreen 组件
  2. Fallback组件从 Suspense 组件中移除

这样原来

Suspense => Offscreen => PriamaryComponent
         => Fragment => FallbackComponent

的结构就变成了 Suspense => Offscreen => PriamaryComponent了。另外因为删除了 FallbackComponent 组件,所以需要创建一个 ChildDeletion 副作用,并将其添加到 Suspense.deletion 数组中 和 添加 Suspense.flags |= ChildDeletion 副作用, 从而在下次 Commit 流程的时候,执行删除操作

  1. 修改 Suspense.child 和 PrimaryComponent.return 的指向

总结

对于 Suspense 组件来说,其有两种状态: PrimaryComponentFallbackComponent,且通过 const didSuspend = (workInProgress.flags & DidCapture)!== NoFlags; 来判断是否需要进入 fallback 状态。

  1. 当初次渲染的时候,其会执行 PrimaryComponent状态的渲染,然后进入到 Lazy 组件的 init(payload) 流程,从而根据 UnIn状态去发起资源的请求 ,并通过 抛出异常的方式,在 handleError() 通过向上遍历的方式找到最近的 Suspense组件,并将其添加 DidCapture副作用。

  2. 在下次渲染的时候,会判断 const didSuspend = (workInProgress.flags & DidCapture)!== NoFlags; 为 true 的情况下,进入 fallback 状态,从而渲染 FallbackComponent 组件。

这时候需要注意的是,在 fallback 状态下,并不是移除初次渲染的 PrimaryComponent 组件,而是将其隐藏(Offscreen 组件的 mode 变成 hidden ),从而避免了 PrimaryComponent 组件的卸载。并形成 Primary 组件和 Fallback 组件的兄弟节点的结构。同时移除 DidCapture副作用。

shell
Suspense => Offscreen => PriamaryComponent
         => Fragment => FallbackComponent`
  1. 下次渲染的时候,如果异步组件还没有加载完成,那么这时候仍然进入 Primary 状态,但是 Primary 组件和fallback组件仍然挂载在 Suspense 组件下
  • 先进入 Lazy 组件的 init(payload) 流程,但是因为其 payload._status === Pending 所以不会发起资源的请求。而是直接进入 Pending 状态, 并抛出异常,重复 2 的流程。
  • 执行 fallback 的清除操作

然后因为存在 DidCapture 副作用,所以再次渲染会又执行 2 的流程

  1. 异步组件加载完成后,会触发 Lazy 组件的状态为加载完成(Resolved),并通过 Ping 触发pingSuspendedRoot 流程,从而再次进入渲染流程。

这时候执行到 Suspense组件的时候,其状态是 Primary 状态,但是 Primary 组件和fallback组件仍然挂载在 Suspense 组件下,所以依次会

  • 执行 Lazy 组件的渲染,这时候发现其状态变成 Resolved ,不会触发异常处理,正常往下走。
  • 执行 fallback 的清除操作

这里面涉及到一个比较重要的组件 Offscreen 组件, 下面我们去看一下这个组件