React-解析Suspence-超简单

957 阅读4分钟

开始

我们都遇到过这样一种场景:当渲染一个组件需要时间比较长的时候,页面就会白屏,直到组件渲染完成才结束。通常我们的做法是...没有什么好的办法

这里指的渲染组件,不是指组件完整显示数据。完整显示数据还需要请求接口,等待接口的请求时间。而我们讨论的不是这个。我们讨论的是jsx -> FunctionComponent -> fiber DOM -> 真实DOM的这段时间,请求数据是发生在真实DOM挂载之后的事情。

如果组件不是很复杂,那么可能只需要一瞬间,也可能长到被用户感知到。

就像下面这段代码:

const Title = () => {
  const sum = fib(40);
  return <div>sum: {sum}</div>;
};

// 斐波那契
const fib = (n) => {
  if (n <= 2) return 1;
  return fib(n - 1) + fib(n - 2);
};

在渲染DOM之前需要先计算斐波那契数列的前40个数字之和(大概需要1.3S)

父组件需要1.3S才能将其渲染出来。如果这个组件占据页面的大部分显示,那就会出现长时间的空白。这不太好

我们有什么办法可以解决这个问题吗?

父组件设置一个变量来检测组件是否加载完成,如果没有加载完成,就显示另外一个DOM?

嗯这是一个思路。

但实际情况是不可行的。因为一个组件生成虚拟DOM的前提是它的所有子组件已经生成了虚拟DOM,所以当一个组件渲染时间很长的话,会阻塞整个fiber树的生成。更别提用什么loading组件来替换了。

suspence的基本使用方法

还好有办法。Suspence官方介绍的一个API,可以实现当子组件没有加载完成的时候,替换成另外一个暂时的组件。这个暂时的组件可以是loading,也可以是骨架屏。

我们来看看如何使用的

除了上面创建的Title组件,我们还需要一个父组件

const Title = React.lazy(() => import("./Title"));

function App() {
  return (
    <div className="App">
      <Suspense fallback={<div>loading ... </div>}>
        <Title />
      </Suspense>
    </div>
  );
}

首先会出现这样的:

过了一秒之后,结果就出来了:

用法很简单,效果也符合预期:这是实现链接:测试Suspence的使用场景

通过React.lazy把Title动态加载进来,并用Suspence包裹,Suspence接收一个属性:fallback。fallback的作用是当Suspence子组件没有渲染完成的时候,就渲染传入fallback的组件

Suspence的实现原理

捕获子组件抛出的异常,如果这个异常是promise,就替换成了fallback组件。当这个promise状态成功之后,就渲染出子组件

我们利用这个特性,来试试异步转同步的写法😁

首先准备好一个提供数据的API

const getName = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("zora");
    }, 1000);
  });
};

一秒后得到结果

然后再写一个包装promise的函数

const handleFetch = (promise) => {
    let value = null;
    let reason = null;
    let status = "pending";
    //get value of promise
    const result = promise
    .then((res) => {
        value = res;
        status = "fulfilled";
    })
    .catch((err) => {
        reason = err;
        status = "rejected";
    });
    
    return () => {
        if (status === "fulfilled") {
            return value;
        } else if (status === "rejected") {
            return reason;
        } else if (status === "pending") {
            throw result;
        }
    };
};

handleFetch函数接收一个promise作为参数,如果这个promise的状态还是pending,就抛出一个新的promise给Suspence。如果状态确定了,就返回promise中的值

下面是一个获取name的组件

import { getName, handleFetch } from "./service";
const namePromise = handleFetch(getName);

const Name = () => {
  
  const name = namePromise();
  
  return <div>my grilfirend's name is: {name}</div>;
};

组件会输出namePromise的返回结果

在App组件中加载Name组件

function App() {

  const Title = lazy(() => import("./Title"));

  return (
    <div className="App">
      <Suspense fallback={<div>loading ... </div>}>
        <Title />
      </Suspense>
      <Suspense fallback={<div>wait a name...</div>}>
        <Name />
      </Suspense>
    </div>
  );
}

这里并没有使用lazy来加载Name组件

这个是效果:

我们可以看到,当获取name的请求没有返回结果,Suspence就会渲染fallback中的组件

OK,我们来捋一捋过程

  1. 首先Name组件会发出获取name的请求,并且在组件内部调用namePromise(),当请求没有返回结果的时候,这个方法会throw 一个promise

  2. Suspence会catch子组件的异常,如果识别到异常时promise,就会渲染fallback,而不会渲染Name组件。这样也就避免了Name直接输出namePromise()返回结果的错误。

    当然,如果异常不是promise,Suspence会继续向外抛出

  3. 之后Suspence会调用catch到的promise的then方法,一旦promise的状态确定,就像Name组件渲染出来,而这时,namePromise()return的值是一个可输出值--'zora'

  4. 完毕

这样的写法看起来很酷

Name组件中发起请求的动作不要放在函数组件内部,会重复发起请求。借用hooks也无济于事。 因为该组件会被反复地挂载、卸载。不过发起请求放在父组件,是个不错的选择

这是实现链接:借助Suspence异步变同步

总结

  1. Suspence的官方推荐场景
  2. 借助Suspence的特性,将发起请求的异步写法改成同步