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。

相關文章
相關標籤/搜索