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 往往会配合 lazy 使用,但是 Suspense 是否只能配合 lazy 使用?lazy 是否又必须配合Suspense? 要搞清楚这两个问题,首先要明白 Suspense 以及 lazy 是在整个过程中扮演的角色,这里先给出一个简单的结论:
加载 -> 过渡 -> 完成后切换
这样一个标准的业务流程。从上面的描述即可以看出,Suspense 是一个加载数据的标准,lazy 只是该标准下实现的一个工具方法。那么说明 Suspense 除配合了 lazy 还可以有其他应用场景。而 lazy 是 Suspense 标准下的一个工具方法,因此无法脱离 Suspense 使用。接下来通过 lazy + Suspense 方式来给大家分析具体原理,搞懂了这部分,我们利用 Suspense 实现自己的数据加载也不是难事。
在深入了解细节之前,我们先了解一下 lazy + Suspense 的基本原理。这里需要一些 react 渲染流程的基本知识。为了统一,在后续将动态加载的组件称为 primary 组件,fallback 传入的组件称为 fallback 组件,与源码保持一致。
参考React实战视频讲解:进入学习
虽然 primary 作为 Suspense 的直接子节点,但是 Suspense 会在 beginWork 阶段直接返回 fallback。使得直接跳过 primary 的遍历。因此此时 primary 必定没有加载完成,所以也没必要再遍历一次。本次渲染结束后,屏幕上会展示 fallback 的内容
这 4 个步骤看起来还是比较复杂。相对于普通的组件主要有两个不同的流程:
不过基本逻辑还是比较简单,即是:
整个 beginWork 遍历顺序为:
Suspense -> primary -> Suspense -> fallback
整个 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,