React 18 新特性(二):Suspense & SuspenseList

本文已收录在 Github: github.com/beichensky/… 中,欢迎 Star,欢迎 Follow!react

前言

本文介绍了 React 18 版本中 Suspense 组件和新增 SuspenseList 组件的使用以及相关属性的用法。而且和 18 以前的版本作了对比,介绍了新特性的一些优点。git

1、回顾 Suspense 用法

早在 React 16 版本,就可使用 React.lazy 配合 Suspense 来进行代码拆分,咱们来回顾一下以前的用法。github

  1. 在编写 User 组件,在 User 组件中进行网络请求,获取数据promise

    User.jsx缓存

    import React, { useState, useEffect } from 'react';
    
    // 网络请求,获取 user 数据
    const requestUser = id =>
        new Promise(resolve =>
            setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 1000)
        );
    
    const User = props => {
        const [user, setUser] = useState({});
    
        useEffect(() => {
            requestUser(props.id).then(res => setUser(res));
        }, [props.id]);
     
        return <div>当前用户是: {user.name}</div>;
    };
    
    export default User;
    复制代码
  2. 在 App 组件中经过 React.lazy 的方式加载 User 组件(使用时须要用 Suspense 组件包裹起来哦)markdown

    App.jsx网络

    import React from "react";
    import ReactDOM from "react-dom";
    
    const User = React.lazy(() => import("./User"));
    
    const App = () => {
        return (
            <> <React.Suspense fallback={<div>Loading...</div>}> <User id={1} /> </React.Suspense> </>
        );
    };
    
    ReactDOM.createRoot(document.getElementById("root")).render(<App />);
    复制代码
  3. 效果图:dom

    Suspense 老版用法图例

  4. 此时,能够看到 User 组件在加载出来以前会 loading 一下,虽然进行了代码拆分,但仍是有两个美中不足的地方异步

    • 须要在 User 组件中进行一些列的操做:定义 stateeffect 中发请求,而后修改 state,触发 renderasync

    • 虽然看到 loading 展现了出来,可是仅仅只是组件加载完成,内部的请求以及用户想要看到的真实数据尚未处理完成

    Ok, 带着这两个问题,咱们继续向下探索。

2、Suspense 的实现原理

内部流程

  • Suspense 让子组件在渲染以前进行等待,并在等待时显示 fallback 的内容

  • Suspense 内的组件子树比组件树的其余部分拥有更低的优先级

  • 执行流程

    • render 函数中可使用异步请求数据

    • react 会从咱们的缓存中读取

    • 若是缓存命中,直接进行 render

    • 若是没有缓存,会抛出一个 promise 异常

    • promise 完成后,react 会从新进行 render,把数据展现出来

    • 彻底同步写法,没有任何异步 callback

简易版代码实现

  • 子组件没有加载完成时,会抛出一个 promise 异常

  • 监听 promise,状态变动后,更新 state,触发组件更新,从新渲染子组件

  • 展现子组件内容

import React from "react";

class Suspense extends React.Component {
    state = {
        loading: false,
    };

    componentDidCatch(error) {
        if (error && typeof error.then === "function") {
            error.then(() => {
                this.setState({ loading: true });
            });
            this.setState({ loading: false });
        }
    }

    render() {
        const { fallback, children } = this.props;
        const { loading } = this.state;
        return loading ? fallback : children;
    }
}

export default Suspense;

复制代码

3、新版 User 组件编写方式

针对上面咱们说的两个问题,来修改一下咱们的 User 组件

const User = async (props) => {
    const user = await requestUser(props.id);
    return <div>当前用户是: {user.name}</div>;
};
复制代码

多但愿 User 组件能这样写,省去了不少冗余的代码,而且可以在请求完成以前统一展现 fallback

可是咱们又不能直接使用 asyncawait 去编写组件。这时候怎么办呢?

结合上面咱们讲述的 Suspense 实现原理,那咱们能够封装一层 promise,请求中,咱们将 promise 做为异常抛出,请求完成展现结果。

wrapPromise 函数的含义:

  • 接受一个 promise 做为参数

  • 定义了 promise 状态和结果

  • 返回一个包含 read 方法的对象

  • 调用 read 方法时,会根据 promise 当前的状态去判断抛出异常仍是返回结果。

function wrapPromise(promise) {
    let status = "pending";
    let result;
    let suspender = promise.then(
        (r) => {
            status = "success";
            result = r;
        },
        (e) => {
            status = "error";
            result = e;
        }
    );
    return {
        read() {
            if (status === "pending") {
                throw suspender;
            } else if (status === "error") {
                throw result;
            } else if (status === "success") {
                return result;
            }
        },
    };
}
复制代码

使用 wrapPromise 从新改写一下 User 组件

// 网络请求,获取 user 数据
const requestUser = (id) =>
    new Promise((resolve) =>
        setTimeout(
            () => resolve({ id, name: `用户${id}`, age: 10 + id }),
            id * 1000
        )
    );

const resourceMap = {
    1: wrapPromise(requestUser(1)),
};

const User = (props) => {
    const resource = resourceMap[props.id];
    const user = resource.read();
    return <div>当前用户是: {user.name}</div>;
};
复制代码

这时候能够看到界面首先展现 loading,请求结束后,直接将数据展现出来。不须要编写反作用代码,也不须要在组件内进行 loading 的判断。

Suspense 新版用法图例

4、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 仍是 hiddenrevealOrder 属性便可生效

    • 子元素中多个 Suspense 不会相互阻塞

SuspenseList 使用

User 组件

import React from "react";

function wrapPromise(promise) {
    let status = "pending";
    let result;
    let suspender = promise.then(
        (r) => {
            status = "success";
            result = r;
        },
        (e) => {
            status = "error";
            result = e;
        }
    );
    return {
        read() {
            if (status === "pending") {
                throw suspender;
            } else if (status === "error") {
                throw result;
            } else if (status === "success") {
                return result;
            }
        },
    };
}

// 网络请求,获取 user 数据
const requestUser = (id) =>
    new Promise((resolve) =>
        setTimeout(
            () => resolve({ id, name: `用户${id}`, age: 10 + id }),
            id * 1000
        )
    );

const resourceMap = {
    1: wrapPromise(requestUser(1)),
    3: wrapPromise(requestUser(3)),
    5: wrapPromise(requestUser(5)),
};

const User = (props) => {
    const resource = resourceMap[props.id];
    const user = resource.read();
    return <div>当前用户是: {user.name}</div>;
};

export default User;
复制代码

App 组件

import React from "react";
import ReactDOM from "react-dom";

const User = React.lazy(() => import("./User"));
// 此处亦能够不使用 React.lazy(),直接使用如下 import 方式引入也能够
// import User from "./User"

const App = () => {
    return (
        <React.SuspenseList revealOrder="forwards" tail="collapsed"> <React.Suspense fallback={<div>Loading...</div>}> <User id={1} /> </React.Suspense> <React.Suspense fallback={<div>Loading...</div>}> <User id={3} /> </React.Suspense> <React.Suspense fallback={<div>Loading...</div>}> <User id={5} /> </React.Suspense> </React.SuspenseList>
    );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
复制代码

使用 SuspenseList 后效果图

SuspenseList 用法图例

相关连接

后记

好了,关于 React 中 Suspense 以及 SuspenseList 组件的用法,就已经介绍完了,在 SuspenseList 使用章节,全部的代码均已贴出来了。有疑惑的地方能够说出来一块儿进行讨论。

文中有写的不对或不严谨的地方,欢迎你们能提出宝贵的意见,十分感谢。

若是喜欢或者有所帮助,欢迎 Star。

相关文章
相关标签/搜索