React 18 新特性(二):Suspense & SuspenseList
前言
本文介绍了 React 18 版本中 Suspense 组件和新增 SuspenseList 组件的使用以及相关属性的用法。并且和 18 之前的版本做了对比,介绍了新特性的一些优势。
回顾 Suspense 用法
早在 React 16 版本,就可以使用 React.lazy 配合 Suspense 来进行代码拆分,我们来回顾一下之前的用法。
- 在编写 User 组件,在 User 组件中进行网络请求,获取数据
User.jsx
1import React, { useState, useEffect } from 'react';
2
3// 网络请求,获取 user 数据
4const requestUser = id =>
5 new Promise(resolve =>
6 setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 1000)
7 );
8
9const User = props => {
10 const [user, setUser] = useState({});
11
12 useEffect(() => {
13 requestUser(props.id).then(res => setUser(res));
14 }, [props.id]);
15
16 return <div>当前用户是: {user.name}</div>;
17};
18
19export default User;
- 在 App 组件中通过 React.lazy 的方式加载 User 组件(使用时需要用 Suspense 组件包裹起来哦)
App.jsx
1import React from "react";
2import ReactDOM from "react-dom";
3
4const User = React.lazy(() => import("./User"));
5
6const App = () => {
7 return (
8 <>
9 <React.Suspense fallback={<div>Loading...</div>}>
10 <User id={1} />
11 </React.Suspense>
12 </>
13 );
14};
15
16ReactDOM.createRoot(document.getElementById("root")).render(<App />);
- 效果图:
4. 此时,可以看到 User 组件在加载出来之前会 loading 一下,虽然进行了代码拆分,但还是有两个美中不足的地方
- 需要在 User 组件中进行一些列的操作:定义 state ,effect 中发请求,然后修改 state,触发 render
- 虽然看到 loading 展示了出来,但是仅仅只是组件加载完成,内部的请求以及用户想要看到的真实数据还没有处理完成
Suspense 的实现原理
内部流程
- Suspense 让子组件在渲染之前进行等待,并在等待时显示 fallback 的内容
- Suspense 内的组件子树比组件树的其他部分拥有更低的优先级
- 执行流程
- 在 render 函数中可以使用异步请求数据
- react 会从我们的缓存中读取
- 如果缓存命中,直接进行 render
- 如果没有缓存,会抛出一个 promise 异常
- 当 promise 完成后,react 会重新进行 render,把数据展示出来
- 完全同步写法,没有任何异步 callback
简易版代码实现
- 子组件没有加载完成时,会抛出一个 promise 异常
- 监听 promise,状态变更后,更新 state,触发组件更新,重新渲染子组件
- 展示子组件内容
1import React from "react";
2
3class Suspense extends React.Component {
4 state = {
5 loading: false,
6 };
7
8 componentDidCatch(error) {
9 if (error && typeof error.then === "function") {
10 error.then(() => {
11 this.setState({ loading: true });
12 });
13 this.setState({ loading: false });
14 }
15 }
16
17 render() {
18 const { fallback, children } = this.props;
19 const { loading } = this.state;
20 return loading ? fallback : children;
21 }
22}
23
24export default Suspense;
新版 User 组件编写方式
针对上面我们说的两个问题,来修改一下我们的 User 组件
1const User = async (props) => {
2 const user = await requestUser(props.id);
3 return <div>当前用户是: {user.name}</div>;
4};
多希望 User 组件能这样写,省去了很多冗余的代码,并且能够在请求完成之前统一展示 fallback
但是我们又不能直接使用 async、await 去编写组件。这时候怎么办呢?
结合上面我们讲述的 Suspense 实现原理,那我们可以封装一层 promise,请求中,我们将 promise 作为异常抛出,请求完成展示结果。
wrapPromise 函数的含义:
- 接受一个 promise 作为参数
- 定义了 promise 状态和结果
- 返回一个包含 read 方法的对象
- 调用 read 方法时,会根据 promise 当前的状态去判断抛出异常还是返回结果。
1function wrapPromise(promise) {
2 let status = "pending";
3 let result;
4 let suspender = promise.then(
5 (r) => {
6 status = "success";
7 result = r;
8 },
9 (e) => {
10 status = "error";
11 result = e;
12 }
13 );
14 return {
15 read() {
16 if (status === "pending") {
17 throw suspender;
18 } else if (status === "error") {
19 throw result;
20 } else if (status === "success") {
21 return result;
22 }
23 },
24 };
25}
使用 wrapPromise 重新改写一下 User 组件
1// 网络请求,获取 user 数据
2const requestUser = (id) =>
3 new Promise((resolve) =>
4 setTimeout(
5 () => resolve({ id, name: `用户${id}`, age: 10 + id }),
6 id * 1000
7 )
8 );
9
10const resourceMap = {
11 1: wrapPromise(requestUser(1)),
12};
13
14const User = (props) => {
15 const resource = resourceMap[props.id];
16 const user = resource.read();
17 return <div>当前用户是: {user.name}</div>;
18};
这时候可以看到界面首先展示 loading,请求结束后,直接将数据展示出来。不需要编写副作用代码,也不需要在组件内进行 loading 的判断。
SuspenseList
上面我们讲述了 Suspense 的用法,那如果有多个 Suspense 同时存在时,我们想控制他们的展示顺序以及展示方式,应该怎么做呢?
React 中也提供了一个新的组件:SuspenseList
SuspenseList 属性
SuspenseList 组件接受三个属性
-
revealOrder: 子 Suspense 的加载顺序
- forwards: 从前向后展示,无论请求的速度快慢都会等前面的先展示
- Backwards: 从后向前展示,无论请求的速度快慢都会等后面的先展示
- together: 所有的 Suspense 都准备好之后同时显示
-
tail: 指定如何显示 SuspenseList 中未准备好的 Suspense
- 不设置:默认加载所有 Suspense 对应的 fallback
- collapsed:仅展示列表中下一个 Suspense 的 fallback
- hidden: 未准备好的项目不限时任何信息
-
children: 子元素
- 子元素可以是任意 React 元素
- 当子元素中包含非 Suspense 组件时,且未设置 tail 属性,那么此时所有的 Suspense 元素必定是同时加载,设置 revealOrder 属性也无效。当设置 tail 属性后,无论是 collapsed 还是 hidden,revealOrder 属性即可生效
- 子元素中多个 Suspense 不会相互阻塞
SuspenseList 使用
User 组件
1import React from "react";
2
3function wrapPromise(promise) {
4 let status = "pending";
5 let result;
6 let suspender = promise.then(
7 (r) => {
8 status = "success";
9 result = r;
10 },
11 (e) => {
12 status = "error";
13 result = e;
14 }
15 );
16 return {
17 read() {
18 if (status === "pending") {
19 throw suspender;
20 } else if (status === "error") {
21 throw result;
22 } else if (status === "success") {
23 return result;
24 }
25 },
26 };
27}
28
29// 网络请求,获取 user 数据
30const requestUser = (id) =>
31 new Promise((resolve) =>
32 setTimeout(
33 () => resolve({ id, name: `用户${id}`, age: 10 + id }),
34 id * 1000
35 )
36 );
37
38const resourceMap = {
39 1: wrapPromise(requestUser(1)),
40 3: wrapPromise(requestUser(3)),
41 5: wrapPromise(requestUser(5)),
42};
43
44const User = (props) => {
45 const resource = resourceMap[props.id];
46 const user = resource.read();
47 return <div>当前用户是: {user.name}</div>;
48};
49
50export default User;
App 组件
1import React from "react";
2import ReactDOM from "react-dom";
3
4const User = React.lazy(() => import("./User"));
5// 此处亦可以不使用 React.lazy(),直接使用以下 import 方式引入也可以
6// import User from "./User"
7
8const App = () => {
9 return (
10 <React.SuspenseList revealOrder="forwards" tail="collapsed">
11 <React.Suspense fallback={<div>Loading...</div>}>
12 <User id={1} />
13 </React.Suspense>
14 <React.Suspense fallback={<div>Loading...</div>}>
15 <User id={3} />
16 </React.Suspense>
17 <React.Suspense fallback={<div>Loading...</div>}>
18 <User id={5} />
19 </React.Suspense>
20 </React.SuspenseList>
21 );
22};
23
24ReactDOM.createRoot(document.getElementById("root")).render(<App />);
使用 SuspenseList 后效果图
下一篇:
React 18 新特性(三):渐变更新
相关笔记