第 26 章:Suspense 与 use——挂起与恢复
第 25 章说 throwException 会把 promise 和真异常分开。promise 那条路,就是 Suspense。本章把"挂起、fallback、恢复"三个动作在源码里对号入座。
<Suspense fallback={<Spinner />}>
<Profile /> {/* 内部 use(fetchProfile()) */}
</Suspense>
先记住这一句:Suspense 是三个动作的循环:挂起(渲染时抛 promise)→ fallback(边界捕获后渲染占位)→ 恢复(promise 完成,ping 触发 RetryLane 重试)。
use让"在渲染里读异步"成为可能,是挂起的标准入口。
一图流:Suspense 的生命周期
挂起:渲染时抛出一个 promise
use(ReactFiberHooks.js:1151)拿到一个 promise 时,会把它抛出去。这个"抛"走进 throwException(ReactFiberThrow.js:362),它一检查发现 value.then 是函数(384),就知道:这不是错误,是挂起。于是走"找最近的 Suspense 边界"分支(getSuspenseHandler,399),当前这次渲染被标记为"挂起"。
注意抛 promise 的机制在第 25 章就出现了:Suspense 和 ErrorBoundary 共用同一个 throwException,只是抛的东西类型不同。这就是为什么 use 和普通错误能"天然区分"。
fallback:边界渲染占位
挂起标记打在哪,updateSuspenseComponent(ReactFiberBeginWork.js:2342)就看哪。它检查 DidCapture 标志(2357):子树挂起了,就把 showFallback 置真,这一次渲染走 fallback children(你的 <Spinner/>),primary 子树(<Profile/>)暂时不渲染。
恢复:promise 完成,ping 唤醒
promise 完成(数据到了)时,React 收到一个 ping:它找到当时挂起的边界,把一次重试更新排进 RetryLanes(ReactFiberLane.js:93,第 22 章那 4 条 Retry 车道)。重试更新以这个 lane 触发一次新渲染,updateSuspenseComponent 再次进来,这次 DidCapture 没了、promise 也有值了,showFallback 为 false,primary 子树重新渲染,fallback 退场。
这套"挂起 → 占位 → ping → 重试"就是 Suspense 的全部。并发渲染(第 23 章)下它更从容:挂起时不急着提交,可以等一等。
19 的变化:use 取代了大量手动边界
React 19 里,很多原本要手动写 Suspense 的场景被 use 简化了:use 能直接在渲染里读 promise,配合编译优化,许多边界被编译期优化掉了,开发者写 <Suspense> 的次数大幅减少。但边界背后的机制(挂起、fallback、ping、Retry)没变,只是更多被编译器代劳了。
动手实验
- 看挂起抛 promise:在
throwException(ReactFiberThrow.js:362)打断点,一个use(fetch())的组件首次渲染,观察 value 是 thenable、走挂起分支。 - 看 showFallback 切换:在
updateSuspenseComponent(2342)打断点,首次渲染看showFallback变 true,数据到了的重试看它变回 false。 - 看 RetryLane:promise resolve 后,在调度处观察这次更新是不是走了 RetryLanes。
小结
- Suspense = 挂起(抛 promise)→ fallback(showFallback)→ 恢复(ping → RetryLane)。
use(1151)是挂起的标准入口,throwException(362)统一分流 promise 和真异常。updateSuspenseComponent(2342)靠DidCapture决定渲染 fallback 还是 primary。- promise resolve 触发 ping,RetryLanes(93)驱动重试渲染。
- 19 用
use优化掉了大量手动边界,机制不变。 - 下一步:useTransition 和 useDeferredValue 怎么配合并发,第 27 章。