标题取自 updateSuspenseComponent 内的注释,该函数是在 beginWork 运行至 Suspense 节点时被调用,其内部逻辑在 First pass 中与 Second pass 中分别有着不同的逻辑(在 The second pass 中其会创建 LazyComponent 上的 fallback 对应的节点),不过在 The first pass 中其会先创建 offScreen,文中的例子在经过 First pass 阶段后,我们可以获得下图结构的 fiber 树
function attachRetryListener(
suspenseBoundary: Fiber,
root: FiberRoot,
wakeable: Wakeable,
lanes: Lanes,
) {
// Retry listener
//
// If the fallback does commit, we need to attach a different type of
// listener. This one schedules an update on the Suspense boundary to turn
// the fallback state off.
//
// Stash the wakeable on the boundary fiber so we can access it in the
// commit phase.
//
// When the wakeable resolves, we'll attempt to render the boundary
// again ("retry").
const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
if (wakeables === null) {
const updateQueue = (new Set(): any);
updateQueue.add(wakeable);
suspenseBoundary.updateQueue = updateQueue;
} else {
wakeables.add(wakeable);
}
}
React Suspense 源码解析
说明
React 版本 v18.1.0
我们称 React.lazy 引入的组件为 lazyComponent , Suspense 上的 fallback 内的组件为 fallbackComponent
分析基于下述代码
TLNR
在 beginWork 创建 fiber 的过程中第一次遭遇 lazyComponent 时会 throw 一个 pending 状态的 promise,该 promise 对象会被外置的 try...catch... ( sync 模式下为 renderRootSync 内、concurrent 模式下为 renderRootConcurrent 内) 捕获,随后向上遍历将途径的所有 fiber 节点打上Incomplete 标签直到 Suspense 节点为止(completeUnitOfWork),再从该节点重新向下进行 beginWork 创建 fiber 而此时将创建 Suspense 的 fallback 节点,随后在 mutation 阶段 (commitMutationEffects) 对先前抛出的 promise 增加成功回掉以支持 异步组件成功加载后重新渲染。
React.lazy
React.lazy 在对 Suspense 的分析过程中并不是什么重要的点,因此只需要参考其 ts 声明了解出入参类型。
如果对示例代码中的 OtherComponent 打点,你可以更直观的看到它的值,其中我们只需要关注 $$typeof 即可。
The first pass
标题取自 updateSuspenseComponent 内的注释,该函数是在 beginWork 运行至 Suspense 节点时被调用,其内部逻辑在 First pass 中与 Second pass 中分别有着不同的逻辑(在 The second pass 中其会创建 LazyComponent 上的 fallback 对应的节点),不过在 The first pass 中其会先创建 offScreen,文中的例子在经过 First pass 阶段后,我们可以获得下图结构的 fiber 树
其中 lazy 这一 fiber 对应的就是通过 React.lazy 引入的 OtherComponent,而此时它还只是一个占位符,不能被称为一个组件。当 beginWork 运行至 lazy 处时才会调用引入组件的逻辑(代码在 mountLazyComponent)。
上面代码块 ctor 对应的函数
对应的调用栈
错误捕获 —— throwException
错误捕获发生在 renderRoot 的 handleError 内,在该函数内的 throwException 会先将 lazy fiber 打上 Incomplete 的标记并找到最近的 Suspense 节点打上 shouldCapture 的标记(该标记将在 The second pass 内起作用)。 将会从 lazy fiber 处开始向上遍历并将沿途的所有 fiber 节点打上 Incomplete 标记,直到遇到 Suspense 节点结束(Suspense 节点除了Incomplete 外还会被标记 shouldCapture,)
如果将整段 handleError 贴上来分析会很长,因此下面我只截取关键步骤以证明
给 lazyComponent 标记 Incomplete
查找最近的 SuspenseComponent 标记 shouldCapture
throwException 内的一些小细节
ConcurrentMode 下的 attachPingListener
attachPingListener 只有在 ConcurrentMode 下才会被调用,因为异步组件的请求有可能在 react 进入 commit 阶段时便返回,而在 ConcurrentMode 模式下是允许高优先级任务插队的,如果返回请求所对应的优先级高于正在进行中的任务,那么就会被插队处理
attachRetryListener
attachRetryListener 主要是将 pending 状态的 promise 挂到 Suspense fiber 的 updateQueue 上,这与其他 fiber 有些不太类似(毕竟都是拿来装 update 实例构成的链表的)。
错误捕获 —— completeUnitOfWork
在 throwException 之后会调用 completeUnitOfWork,该函数在此处的作用是从 lazy Component 出发向上遍历并将沿途的所有 fiber 节点都标记上 Incomplete 标记,但当其遇到带有 ShouldCapture 标记的节点后便会将其上关于错误处理的标记消除(next.flags &= HostEffectMask)然后再从这个节点进入 beginWork。其也是 completeWork 的入口函数,是 react 中的一个关键函数。
The second pass
在经过 completeUnitOfWork 的处理后,Suspense 节点上已经被标记了 DidCapture,而在 The second pass 中 beginWork 会再次从 Suspense 节点开始创建 fiber 节点(updateSuspenseComponent ),由于 DidCapture 的影响,updateSuspenseComponent 也会进入 first pass 跳过的逻辑去处理 fallback 的内容。
mountSuspenseFallbackChildren 内的大致逻辑
在经过上述逻辑后,其结构如下
而在 The second pass 结束后,其 fiber 树结构为下图
关于 promise resolve 之后触发的重新渲染
当异步组件加载结束后,也就是之前那个 pending 的 promise 实例状态变更后,需要添加一个对应的成功回掉以触发更新,而这个回掉函数是在 commit 阶段(commitRootImpl)内的 mutation 小阶段(commitMutationEffects)内注册进 promise 内的,具体位置存在于 attachSuspenseRetryListeners 内。
图中的 retry 函数为 resolveRetryWakeable (位置在 ReactFiberWorkLoop.old.js 内)
ensureRootIsScheduled 相关的可以看我的 [React Hooks: useState 分析](https://github.com/IWSR/react-code-debug/issues/3)