suspense的字面意思就是悬而不决,用在平时开发中,就可以理解为还没有完成的事,你不知道啥时候完成。也就是异步,异步加载组件,异步请求数据。
Suspense是用来做什么的?
1. 代码拆分
服务于打包优化的代码拆分。lazy和suspense配合使用
const A = React.lazy(() => import('./A'))
return (
<Suspense fallback={<p>loading</p>}>
<Route component={A} path="/a">
</Suspense>
)
这样在打包代码时,可以显著减少主包的体积,加快加载速度,从而提升用户体验;而当路由切换时,加载新的组件代码,代码加载是异步的过程,此时suspense就会进如fallback,那我们看到的就是loading,显式的告诉用户正在加载,当代码加载完成就会展示A组件的内容,整个loading状态不用开发者去控制。
2. 异步加载数据
既然代码拆分可以不用维护loading,那么加载数据是不是也可以不用维护loading状态呢?
当然可以!
但是直接用axios或者fetch是无法进入suspense的fallback的,但是react提供了一个库供我们使用react-cache(暂不建议使用的),它具体是做什么的,原理是什么,我们后面在讨论,这里先体验一下效果如何。
- 现在用的方法,管理loading状态
function A() {
const [loading, setLoading] = useState(false)
const [list, setList] = useState([])
function getList() {
setloading(true)
promise.then(res => {
setList(res)
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => {
getList()
}, [])
return (
<>
data...
{loading && <p>loading</p>}
</>
)
}
- 使用suspense处理数据加载
const resource = unstable_createResource((id) => {
return Promise.resolve([])
})
function B() {
const data = resource.read(0)
return (
<Suspense fallback={<p>loading</p}>
data...
</Suspense>
)
}
可以看到,使用suspense的方式,在开发的时候完全不用维护loading状态,而且还有一个比较大的差别,B中的list是没有使用state的,它获取的点是在B渲染时,而A获取数据则是发生在A渲染后
细谈Suspense
1. 使用方式(触发方式)
- react-cache
为什么直接使用axios或者fetch不能进入suspense的fallback呢,而使用react-cache包一下请求,就可以进入fallback了呢?
来康康react-cache的源码
read: function (input) {
// react-cache currently doesn't rely on context, but it may in the
// future, so we read anyway to prevent access outside of render.
readContext(CacheContext);
var key = hashInput(input);
var result = accessResult(resource, fetch, input, key);
switch (result.status) {
case Pending:
{
var suspender = result.value;
throw suspender;
}
case Resolved:
{
var _value = result.value;
return _value;
}
case Rejected:
{
var error = result.value;
throw error;
}
default:
// Should be unreachable
return undefined;
}
},
这里我截取了部分源码,可以看到这是一个类似promise的结构,有三种状态,而需要注意的是pending状态。
首先看看这个result是这个什么东西,可以理解为result.value就是传入的数据请求方法,执行返回的promise或者说likePromise,当这个promise未决时,会throw出这个promise,而此时suspense看到这个promise自然就知道还处于数据请求中,就会展示fallback中的内容,当这个promise已决时,则代表数据请求结束,suspense就应该展示数据内容。
大致的suspense工作的流程就是:
1. 事先throw
2. 在 completeWork 之前 catch 住
3. 然后添加到 updateQueue 里
4. updateQueue 批量更新
这也就解释了为什么直接使用axios、fetch无法展示suspense的fallback。
在理解原理之后,你可以很轻易的写出类似于这样的代码,替代一下react-cache
function promiseWrapper(promise) {
let status = "pending";
let result;
let likePromise = promise.then(
r => {
status = "success";
result = r;
},
e => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw likePromise;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
}
};
}
- Relay
- swr
2. 组件渲染顺序
- 挂载后渲染
- 获取数据后渲染
- 并行获取数据和渲染组件
上面说到组件的渲染顺序,在没使用suspense时,数据获取是在组件渲染完成之后进行的。而使用suspense的方式,组件是如何渲染的?
// 官方示例
const resource = http()
function Foo () {
const result = resource.foo.read()
return <div>Foo: { result.name }</div>
}
function Bar () {
const result = resource.bar.read()
return <div>Bar: { result.name }</div>
}
function Page () {
return (
<React.Suspense fallback={<h1>Loading Foo...</h1>}>
<Foo/>
<React.Suspense fallback={<h1>Loading Bar...</h1>}>
<Bar/>
</React.Suspense>
</React.Suspense>
)
}
3. race condition
React 组件有它们自己的“生命周期”。组件可能在任意时间点接收到 props 或者更新 state。然而,每一个异步请求同样也有自己的“生命周期”。异步请求的生命周期开始于我们发出请求,结束于我们收到响应报文。
4. 错误边界
由于以上请求的调用方式,们不等待promise就直接开始渲染,使得我们不太好使用promise.catch去捕获异步中的错误,所以得另辟蹊径
getDerivedStateFromError生命周期函数允许我们捕获组件中出现的错误情况,所以我们可以很方便的写出一个类似这样的ErrorBoundary处理组件,去处理下面层级中出现的错误情况。
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return {
hasError: true,
error
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
总结
说了那么多,Suspense 到底有什么用呢?对于这个问题,可以从不同的角度来回答:
- 它能让数据获取库与 React 紧密整合。如果一个数据请求库实现了对 Suspense 的支持,那么,在 React 中使用 Suspense 将会是自然不过的事。
- 它能让你有针对性地安排加载状态的展示。虽然它不干涉数据怎样获取,但它可以让你对应用的视图加载顺序有更大的控制权。
- 它能够消除 race conditions。即便是用上 await,异步代码还是很容易出错。相比之下,Suspense 更给人同步读取数据的感觉 —— 假定数据已经加载完毕。
总之,我更认为Suspense是一种改变你开发模式的,更好的UI解耦的一种模式。