Suspense 是用来做什么的?

suspense 的字面意思就是悬而不决,用在平时开发中,就可以理解为还没有完成的事,你不知道啥时候完成。也就是异步,异步加载组件,异步请求数据。

1. 代码拆分

服务于打包优化的代码拆分。lazysuspense配合使用

1
2
3
4
5
6
7
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 是无法进入 suspensefallback 的,但是 react 提供了一个库供我们使用 react-cache(暂不建议使用的),它具体是做什么的,原理是什么,我们后面在讨论,这里先体验一下效果如何。

现在用的方法,管理 loading 状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 处理数据加载
1
2
3
4
5
6
7
8
9
10
11
12
13
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 的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 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 工作的流程就是:
markdown 代码解读复制代码 1. 事先 throw 2. 在 completeWork 之前 catch 住 3. 然后添加到 updateQueue 里 4. updateQueue 批量更新

这也就解释了为什么直接使用 axios、fetch 无法展示 suspense 的 fallback。
在理解原理之后,你可以很轻易的写出类似于这样的代码,替代一下 react-cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
},
};
}

2. 组件渲染顺序

挂载后渲染
获取数据后渲染
并行获取数据和渲染组件

上面说到组件的渲染顺序,在没使用 suspense 时,数据获取是在组件渲染完成之后进行的。而使用 suspense 的方式,组件是如何渲染的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 官方示例
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 处理组件,去处理下面层级中出现的错误情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 代码解读复制代码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 到底有什么用呢?对于这个问题,可以从不同的角度来回答:

1. 它能让数据获取库与 React 紧密整合。如果一个数据请求库实现了对 Suspense 的支持,那么,在 React 中使用 Suspense 将会是自然不过的事。
2. 它能让你有针对性地安排加载状态的展示。虽然它不干涉数据怎样获取,但它可以让你对应用的视图加载顺序有更大的控制权。
3. 它能够消除 race conditions。即便是用上 await,异步代码还是很容易出错。相比之下,Suspense 更给人同步读取数据的感觉 —— 假定数据已经加载完毕。

总之,我更认为Suspense是一种改变你开发模式的,更好的UI解耦的一种模式。