React Suspense 解析
6 月 22, 2022 • ☕️☕️☕️ 20 min read
现在我们代码中有「副作用」的行为例如异步请求,都是放在 useEffect 中处理的,而 Suspense 通常只是搭配 React Lazy 来实现代码分割。但是在未来,我们或许可以完全依赖 Suspense,而不再关心什么样的代码是有副作用的。
Suspense 如何使用
我们通过在组件里抛出一个 Promise 的异常,Suspense 会帮我们捕获到这个异常,然后显现 fallback 的内容,因为我们的组件永远都是在抛出 Promise 的异常,所以我们的页面会一直展示 loading...
function ProfilePage() {
return (
<Suspense fallback={<h1>loading...</h1>}>
<ProfileDetails />
</Suspense>
)
}
const promise = new Promise(() => {})
function ProfileDetails() {
throw promise
return <h1>Hello World</h1>
}

我们声明一个 done 的变量,false 的时候抛出异常展示 fallback 的内容,等异步执行结束后设置为 true,这时候页面展示 Hello World。
function ProfilePage() {
return (
<Suspense fallback={<h1>loading...</h1>}>
<ProfileDetails />
</Suspense>
)
}
let done = false
const promise = new Promise((resolve) => {
setTimeout(() => {
done = true
resolve()
}, 1000)
})
function ProfileDetails() {
if (!done) {
throw promise
}
return <h1>Hello World</h1>
}
通过这个思路,我们可以封装一个通用的函数,传入一个 promise,在 promise resolve 之前抛出异常,resolve 以后返回正常结果。
function wrapPromise(promise) {
let status = 'pending'
let result
let suspender = promise.then(
(r) => {
status = 'success'
result = r
},
(e) => {
status = 'error'
result = e
}
)
return {
read() {
if (status === 'pending') {
throw suspender
} else if (status === 'error') {
throw result
} else if (status === 'success') {
return result
}
},
}
}
这时我们就可以像写一个同步的代码一样来请求数据,在数据请求回来之前展示 Suspense 的 fallback 内容,数据请求回来以后展示正常的数据。
function fetchProfileData() {
// fetchUser 是一个普通的异步请求
let userPromise = fetchUser()
// 使用 wrapPromise 包装
return wrapPromise(userPromise)
}
const resource = fetchProfileData()
function ProfilePage() {
return (
<Suspense fallback={<h1>loading...</h1>}>
<ProfileDetails />
</Suspense>
)
}
function ProfileDetails() {
console.log('ProfileDetails render')
const user = resource.read()
console.log('user: ', user)
return <h1>{user.name}</h1>
}

Suspense 原理
在知道了如何使用 Suspense 以后,我们不禁有两个疑问。
- react 是如何捕获到抛出的 Promise。
- react 是如何在 Promise 执行完成以后重新展示正确的内容。
我们可以通过一段简单的代码来解释上述两个问题,可以看到我们通过 try... catch... 来捕获异常,然后在 catch 的逻辑中执行这个 promise,在回调中重新执行 render 方法,这时候 done 已经变成 true,就可以正常执行 render 了。
let done = false
const promise = new Promise((resolve) => {
setTimeout(() => {
done = true
resolve()
}, 1000)
})
function render() {
try {
if (!done) {
console.log('loading...')
throw promise
}
console.log('render done')
} catch (error) {
promise.then((res) => render())
}
}
render()
同样的,react 也使用了一样的思路,让我们先看一下 react 执行的大体流程,然后再逐步看下具体实现。

1. 捕获异常
在组件 render 的过程使用 try... catch... 捕获 render 阶段抛出的异常。
do {
try {
// 组件 render 流程
workLoopSync()
break
} catch (thrownValue) {
handleError(root, thrownValue)
}
} while (true)
在渲染的过程中第一次遇到 Suspense 组件会正常渲染其子孙结点。
// showFallback 为 false
if (showFallback) {
// 渲染fallback
} else {
return mountSuspensePrimaryChildren(workInProgress, nextPrimaryChildren, renderLanes)
}
在 render 到子组件时 try... catch... 捕获到抛出的异常,通过判断捕获的异常有没有 then 方法来判断是否进入 Suspense 的逻辑,
if (value !== null && typeof value === 'object' && typeof value.then === 'function') {
// 是否需要 Suspense 接管
}
2. 处理 Suspense 组件
如果是需要 Suspense 接管,则会依次执行以下三步:
- 查找抛出异常组件最近的
Suspense。 - 对
Suspense做标记,再次 render 则展示 fallback 的内容。 - 将捕获的 Promise 挂载到
Suspense上。
// 查找当前抛出异常的组件最近的 Suspense
const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber)
// 给查找到的Suspense做一个标记
markSuspenseBoundaryShouldCapture(suspenseBoundary, returnFiber, sourceFiber, root, rootRenderLanes)
// 将捕获的异常挂载到Suspense的updateQueue上
attachRetryListener(suspenseBoundary, root, wakeable, rootRenderLanes)
在执行完上述三步以后会重新执行一遍 render。
completeUnitOfWork(erroredWork)
3. 展示 fallback
在执行第二次 render 的时候由于 Suspense 已经被打标记,说明已经捕获到错误了,此时展示 fallback 的内容,因为不再渲染有异常的子组件,所以 render 可以继续向下执行。
// showFallback 为 true
if (showFallback) {
const fallbackFragment = mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes
)
return fallbackFragment
}
4. 执行 promise
在最后的 commit 阶段,执行在 Suspense 挂载的 promise。
function attachSuspenseRetryListeners(finishedWork: Fiber) {
const wakeables: Set<Wakeable> | null = (finishedWork.updateQueue: any);
if (wakeables !== null) {
wakeables.forEach(wakeable => {
const retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);
if (!retryCache.has(wakeable)) {
// 关键代码
wakeable.then(retry, retry);
}
});
}
}
5. 重新 render
最后当 promise resolve 结束后会重新 render Suspense 的子孙组件,这时数据请求已经完成,子孙组件不再抛出异常,页面正常渲染有数据的结果。
总结
Suspense 是 React 一个非常重要的特性,Suspense 的存在或许会改变我们未来数据请求的方式。