beichensky / Blog

经常写博客的地方,会时常记录一些学习笔记、技术博客或者踩坑历程。
181 stars 12 forks source link

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

Open beichensky opened 3 years ago

beichensky commented 3 years ago

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

前言

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

一、回顾 Suspense 用法

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

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

    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 组件包裹起来哦)

    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. 效果图:

    Suspense 老版用法图例

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

    • 需要在 User 组件中进行一些列的操作:定义 stateeffect 中发请求,然后修改 state,触发 render

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

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

二、Suspense 的实现原理

内部流程

简易版代码实现

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;

三、新版 User 组件编写方式

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

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

多希望 User 组件能这样写,省去了很多冗余的代码,并且能够在请求完成之前统一展示 fallback

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

结合上面我们讲述的 Suspense 实现原理,那我们可以封装一层 promise,请求中,我们将 promise 作为异常抛出,请求完成展示结果。

wrapPromise 函数的含义:

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 新版用法图例

四、SuspenseList

上面我们讲述了 Suspense 的用法,那如果有多个 Suspense 同时存在时,我们想控制他们的展示顺序以及展示方式,应该怎么做呢?

React 中也提供了一个新的组件:SuspenseList

SuspenseList 属性

SuspenseList 组件接受三个属性

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。

onlyU-uaena commented 2 years ago
componentDidCatch(error) {
        if (error && typeof error.then === "function") {
            error.then(() => {
                this.setState({ loading: true });
            });
            this.setState({ loading: false });
        }
    }

这个位置 true 和 false 的 loading 态是不是反了