React suspense用法詳解

React.suspense是你們用的比較少的功能,它早在2018年的16.6.0版本中就已發佈。它的相關用法有些已經比較成熟,有的相對不太穩定,甚至經歷了重命名、刪除。前端

下面一塊兒來了解下它的主要用法、場景。react

1.suspense配合lazy實現code spliting

import是webpack中的一種code spliting的用法,可是import的文件返回的是一個promise,必須封裝以後才能使用,例如react-loadable的封裝方法webpack

function Loadable(opts) {
  const { loading: LoadingComponent, loader } = opts
  return class LoadableComponent extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        loading: true, // 是否加載中
        loaded: null  // 待加載的模塊
      }
    }
    componentDidMount() {
      loader()
        .then((loaded) => {
          this.setState({
            loading: false,
            loaded
          })
        })
        .catch(() => {})
    }

    render() {
      const { loading, loaded } = this.state
      if (loading) {
        return <LoadingComponent />
      } else if (loaded) {
        // 默認加載default組件
        const LoadedComponent = loaded.__esModule ? loaded.default : loaded;
        return <LoadedComponent {...this.props}/>
      } else {
        return null;
      }
    }
  }
}
複製代碼

在promise返回後更新組件,若是使用suspense改寫react-loadable,將會更加優雅web

const ProfilePage = React.lazy(() =>  import('./ProfilePage'));


<Suspense fallback={<Spinner />}>

  <ProfilePage />

</Suspense>
複製代碼

2.1 請求數據時解決loading問題

let status = "pending";
let result;
const data = new Promise(resolve => setTimeout(() => resolve("結果"), 1000));

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


function App(){
    const state = wrapPromise(data);
    
  return (<div>{state}</div>);
}

function Loading(){
    return <div>..loading</div>
}

class TodoApp extends React.Component {
  
  render() {
    return (
      <React.Suspense fallback={<Loading></Loading>}> 
        <App />
      </React.Suspense>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))
複製代碼

源碼在此
上面的寫法比較奇怪,在組件App中請求數據state時,一開始返回throw promise,這是爲了讓suspense捕捉到error,返回loading組件,以上寫法跟suspense的實現方式有關api

class Suspense extends React.Component { 
    state = { promise: null } 
    componentDidCatch(e) { 
        if (e instanceof Promise) { 
            this.setState(
            { promise: e }, () => { 
                e.then(() => { 
                    this.setState({ promise: null }) 
                }) 
            }) 
        } 
    } 
    render() { 
        const { fallback, children } = this.props 
        const { promise } = this.state 
        return <> 
            { promise ? fallback : children } 
        </> 
    } 
}
複製代碼

從suspense源碼能夠看出,suspense捕捉到error後,會對其監聽,當返回值時將loading改成children中的組件。
但這時又會觸發一次組件渲染,因此須要對請求結果緩存,最終變成上面的寫法。
這裏有個官方例子可供參考,傳送門promise

2.2 使用react-cache緩存

上面的例子很是反人類,在實際項目中基本不可能這樣寫,配合react-cache將會優雅許多緩存

import React, { Suspense } from "react";
import { unstable_createResource as createResource } from "react-cache";

const mockApi = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("Hello"), 1000);
  });
};

const resource = createResource(mockApi);

const Greeting = () => {
  const result = resource.read();

  return <div>{result} world</div>;
};

const SuspenseDemo = () => {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Greeting />
    </Suspense>
  );
};

export default SuspenseDemo;
複製代碼

react-cache官方目前不推薦使用在線上項目中app

3.配合ConcurrentMode解決loading的閃現問題

loading的閃現問題主要是由於api接口時間短,loading不應出現,須要對接口速度進行判斷dom

不考慮suspense按照一般的寫法,能夠這麼實現async

const timeout = ms => new Promise((_, r) => setTimeout(r, ms));

const rq = (api, ms, resolve, reject) => async (...args) => {
  const request = api(...args);
  Promise.race([request, timeout(ms)]).then(resolve, err => {
    reject(err);
    return request.then(resolve);
  });
};
複製代碼

suspense爲咱們提供了maxDuration屬性,用來控制loading的觸發時間

import React from "react";
import ReactDOM from "react-dom";
const {
  unstable_ConcurrentMode: ConcurrentMode,
  Suspense,
} = React;
const { unstable_createRoot: createRoot } = ReactDOM;

let status = "pending";
let result;
const data = new Promise(resolve => setTimeout(() => resolve("結果"), 3000));

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


function Test(){
    const state = wrapPromise(data);
    
  return (<div>{state}</div>);
}

function Loading(){
    return <div>..loading</div>
}

class TodoApp extends React.Component {
  
  render() {
    return (
      <Suspense fallback={<Loading></Loading>} maxDuration={500}> 
        <Test />
      </Suspense>
    )
  }
}

const rootElement = document.getElementById("root");

createRoot(rootElement).render(
  <ConcurrentMode>
    <TodoApp />
  </ConcurrentMode>
);
複製代碼

源碼地址在此

上面例子使用的是16.8.0版本

例子中用到了unstable_ConcurrentModeunstable_createRoot語法,unstable_createRoot在16.11.0中已改名爲createRoot,unstable_ConcurrentMode在16.9.0中改名爲unstable_createRoot
在最新16.13.1中測試發現ReactDOM.createRoot並不存在,因此本例子只在16.8.0中測試

總結

以上就是關於suspense的全部場景,目前api善不穩定,謹慎使用

招聘

最近字節跳動前端急招,有感興趣的請私信我,或者投遞我郵箱574745389@qq.com
前端base上海、北京、南京、深圳、杭州,崗位要求可參考job.toutiao.com/s/7wokvh
除了前端其餘崗位的也歡迎投遞

相關文章
相關標籤/搜索