Suspense
介绍
React16.6之后新增了<Suspense />组件,可以作为组件的等待边界,当组件是从服务端加载或者懒加载时,会渲染fallback内容,避免白屏,等待加载完成之后显示组件内容。
一般都是与lazy()配合使用,该方法是用来加载某个组件的,结合一些Boundler工具可以进行代码分割等优化操作。
function lazy<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;该方法目前(React18)只能和<Suspense />组件使用(当然,要是实现一个和Suspense组件类似的也能一起使用)。
但是Suspense能和其他数据加载的方式一起使用:
import { Suspense } from 'react'
let data = ''
function fetchData() {
if(data) return data
throw new Promise((resolve) => {
setTimeout(() => {
data = 'fetch data'
resolve('')
}, 2000)
})
}
function Content() {
return <>{fetchData()}</>
}
const SuspenseWrapper = () => {
return (
<Suspense fallback={<>Loading...</>}>
<Content />
</Suspense>
)
}页面会先显示fallback内容,然后2s后显示Content内容。
原理解析
从上面的简单demo中可以看出一些Suspense的核心原理:子组件抛出一个Promise(throw new Promise()),Suspense通过错误捕获,检查类型是一个Promise,随后对这个Promise追加一个then方法,在then方法中再次更新Suspense。然后将fallback作为子组件的兄弟节点渲染到页面上,等到Promise状态变化之后,会触发then的执行,再次更新Suspense,展示出子组件的内容,并卸载掉fallback组件。
具体流程如下:
- 在React执行
beginWork进行深度遍历组件生成Fiber树时,当遍历到Suspense组件,待加载的组件作为其子组件,下一个应该遍历该组件。 - 当遍历到该待加载的组件时,由于该组件未加载完成会抛出一个异常,该异常为
Promise,React捕获到该异常会判断类型,如果是Promise会对其追加一个then方法。在then中再次触发Suspense的更新。React同时还会将下一个遍历的元素重新设为Suspense。 - 继续
beginWork流程,再次遍历到Suspense时,将fallback以及待加载的组件都生成,并直接返回fallback,跳过待加载组件的遍历(因为已经遍历过一遍了)。本次渲染结束之后,页面上会展示fallback的内容。 - 当待加载组件加载完成之后,会触发
then回调,再次更新Suspense,直接渲染加载后的组件,并卸载fallback组件。
一次beginWork流程,遍历的组件顺序如下:
Suspense -> 待加载组件 -> Suspense -> fallback待加载组件
核心原理就分为两部分:
- 加载完成,返回组件
- 否则(正在加载以及加载失败),抛出异常,异常内容为一个Promise
异常的捕获
React的reconcil阶段是在workLoop中进行的
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);包裹了一层try..catch,throw的异常会被捕获,进入handleError中处理。如果是Promise,会做这些事情:
- 获取到最近的
Suspense父级组件,没有会报错。 - 找到之后,给父级组件打上一些标记,意为需要渲染
fallback(二次遍历Suspense)。 - 将抛出的Promise添加到
Suspense父组件的updateQueue中,后续会对这个Promise进行then的追加 - 遍历完之后会进入
completeWork的流程,根据Suspense打上的标记,将下一次遍历的节点还设置为Suspense。这一步主要是为了生成fallback组件。
添加Promise的then回调
上面说到会将Promise添加到父级Suspense的updateQueue中,进入到commit阶段会遍历updateQueue,为每一个都添加回调,回调执行时会触发Suspense组件的更新
Suspense
说完了上面几个的原理,Suspense的原理就是将这几个东西串起来的。
首屏渲染(mount):
beginWork遍历到Suspense时,会访问子节点(待加载节点),此时子节点正在加载会抛出异常,捕获之后会将下一次遍历节点还设为Suspensereact^18.3.1中 是将
Suspense放到了子组件最后,即会遍历完所有的子组件,再回到Suspense。但是也有人说 貌似是个bug,等到版本更新之后再验证第二次遍历到
Suspense时,将fallback节点当做待加载节点的兄弟节点,直接返回fallback节点,避免第二次访问子节点。
子节点加载前的更新:
即其他组件触发的Suspense更新
- 第一次遍历到
Suspense时,如果fallback组件有值,就将其添加到Suspense组件的待删除队列deletions中。其他和上面一样 - 第二次遍历到
Suspense时,会清掉待删除队列deletions。其他和上面也是一样的
子节点加载后的更新:
加载完成会触发then回调,再次更新Suspense
- 第一次遍历到
Suspense时,都是一样的。但是由于已经加载完成,并没有抛出异常,所以不会第二次遍历Suspense,fallback组件会被卸载,显示子组件