Suspense 基本应用
Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成。其中 lazy 接受类型为 Promise<() => {default: ReactComponet}>
的参数,并将其包装为 react 组件。ReactComponet 可以是类组件函数组件或其他类型的组件,例如:
const Lazy = React.lazy(() => import("./LazyComponent"))
<Suspense fallback={
"loading"}>
<Lazy/> // lazy 包装的组件
</Suspense>
由于 Lazy 往往是从远程加载,在加载完成之前 react 并不知道该如何渲染该组件。此时如果不显示任何内容,则会造成不好的用户体验。因此 Suspense 还有一个强制的参数为 fallback,表示 Lazy 组件加载的过程中应该显示什么内容。往往 fallback 会使用一个加载动画。当加载完成后,Suspense 就会将 fallback 切换为 Lazy 组件的内容。一个完整的例子如下:
function LazyComp(){
console.info("sus", "render lazy")
return "i am a lazy man"
}
function delay(ms){
return new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
}
// 模拟动态加载组件
const Lazy = lazy(() => delay(5000).then(x => ({
"default": LazyComp})))
function App() {
const context = useContext(Context)
console.info("outer context")
return (
<Suspense fallback={
"loading"}>
<Lazy/>
</Suspense>
)
}
这段代码定义了一个需要动态加载的 LazyComp 函数式组件。会在一开始显示 fallback 中的内容 loading,5s 后显示 i am a lazy man。
Suspense 原理
虽然说 Suspense 往往会配合 lazy 使用,但是 Suspense 是否只能配合 lazy 使用?lazy 是否又必须配合Suspense? 要搞清楚这两个问题,首先要明白 Suspense 以及 lazy 是在整个过程中扮演的角色,这里先给出一个简单的结论:
- Suspense: 可以看做是 react 提供用了加载数据的一个标准,当加载到某个组件时,如果该组件本身或者组件需要的数据是未知的,需要动态加载,此时就可以使用 Suspense。Suspense 提供了
加载 -> 过渡 -> 完成后切换
这样一个标准的业务流程。 - lazy: lazy 是在 Suspense 的标准下,实现的一个动态加载的组件的工具方法。
从上面的描述即可以看出,Suspense 是一个加载数据的标准,lazy 只是该标准下实现的一个工具方法。那么说明 Suspense 除配合了 lazy 还可以有其他应用场景。而 lazy 是 Suspense 标准下的一个工具方法,因此无法脱离 Suspense 使用。接下来通过 lazy + Suspense 方式来给大家分析具体原理,搞懂了这部分,我们利用 Suspense 实现自己的数据加载也不是难事。
基本流程
在深入了解细节之前,我们先了解一下 lazy + Suspense 的基本原理。这里需要一些 react 渲染流程的基本知识。为了统一,在后续将动态加载的组件称为 primary 组件,fallback 传入的组件称为 fallback 组件,与源码保持一致。
- 当 react 在 beginWork 的过程中遇到一个 Suspense 组件时,会首先将 primary 组件作为其子节点,根据 react 的遍历算法,下一个遍历的组件就是未加载完成的 primary 组件。
- 当遍历到 primary 组件时,primary 组件会抛出一个异常。该异常内容为组件 promise,react 捕获到异常后,发现其是一个 promise,会将其 then 方法添加一个回调函数,该回调函数的作用是触发 Suspense 组件的更新。并且将下一个需要遍历的元素重新设置为 Suspense,因此在一次 beginWork 中,Suspense 会被访问两次。
- 又一次遍历到 Suspense,本次会将 primary 以及 fallback 都生成,并且关系如下:
参考React实战视频讲解:进入学习
虽然 primary 作为 Suspense 的直接子节点,但是 Suspense 会在 beginWork 阶段直接返回 fallback。使得直接跳过 primary 的遍历。因此此时 primary 必定没有加载完成,所以也没必要再遍历一次。本次渲染结束后,屏幕上会展示 fallback 的内容
- 当 primary 组件加载完成后,会触发步骤 2 中 then,使得在 Suspense 上调度一个更新,由于此时加载已经完成,Suspense 会直接渲染加载完成的 primary 组件,并删除 fallback 组件。
这 4 个步骤看起来还是比较复杂。相对于普通的组件主要有两个不同的流程:
- primary 会组件抛出异常,react 捕获异常后继续 beginWork 阶段。
- 整个 beginWork 节点,Suspense 会被访问两次
不过基本逻辑还是比较简单,即是:
- 抛出异常
- react 捕获,添加回调
- 展示 fallback
- 加载完成,执行回调
- 展示加载完成后的组件
整个 beginWork 遍历顺序为:
Suspense -> primary -> Suspense -> fallback
源码解读 - primary 组件
整个 Suspend 的逻辑相对于普通流程实际上是从 primary 组件开始的,因此我们也从 react 是如何处理 primary 组件开始探索。找到 react 在 beginWork 中处理处理 primary 组件的逻辑的方法 mountLazyComponent
,这里我摘出一段关键的代码:
const props = workInProgress.pendingProps;
const lazyComponent: LazyComponentType<any, any> = elementType;
const payload = lazyComponent._payload;
const init = lazyComponent._init;
let Component = init(payload); // 如果未加载完成,则会抛出异常,否则会返回加载完成的组件
其中最关键的部分莫过于这个 init 方法,执行到这个方法时,如果没有加载完成就会抛出 Promise 的异常。如果加载完成就直接返回完成后的组件。我们可以看到这个 init 方法实际上是挂载到 lazyComponent._init
方法,lazyComponent 则就是 React.lazy() 返回的组件。我们找到 React.lazy() :
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: Uninitialized,
_result: ctor,
};
const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,