React Suspense 在异步获取数据方面的应用

樊琦
2023-12-01

写这篇文章之前,实在想吐槽一下 React 官方文档,很多 API 的介绍都是不明不白的,不看之前有一堆问号,看完之后问号更多了。

项目需求

最近在开发一个大数据运维的项目,需要展示大量的 echarts 图表。由于从后台调接口返回数据需要时间,然后前端对数据的处理也需要时间,这样就导致了页面加载的延迟。希望可以做一个整页的 loading ,在图表渲染完成之后展示图表内容。最近在看 ssh 大佬写的一篇文章:

React Suspense + 自定义Hook开启数据请求新方式

看完之后深受启发,希望可以借鉴这个思路实现上述需求。

什么是 React.Suspense

根据 React 官网的介绍,React.Suspense 可以用于在懒加载组件渲染之前先展示一个加载指示器(loading indicator),需要搭配 React.lazy 进行使用:

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <OtherComponent />
    </React.Suspense>
  );
}

看到这里就有两个问题:

  • React.Suspense 是通过什么机制知道里面异步加载的组件已经加载完毕的;
  • 为什么异步加载的组件要通过 React.lazy 包裹一下(在 Vue 的路由懒加载直接通过 () => import(...) 就可以了);

在 React 官网上有一个说明:

使用 React.lazy 的动态引入特性需要 JS 环境支持 Promise。在 IE11 及以下版本的浏览器中需要通过引入 polyfill 来使用该特性

从上面的描述中可以得知,React.lazy 里面应该是用到了 Promise ,但并不清楚具体的实现机制。

什么是 SWR

首先 swr 是一个用于获取远程数据的 Hooks :

swr - npm

这个库除了 API 的封装比较优雅,更重要的是支持 swr 请求策略。那么 swr 的全称是 stale-while-revalidate ,顾名思义就是重新请求的同时使用过期数据,也就是在发送 HTTP 请求时先返回上一次请求的数据,等后台返回了再把之前的数据替换掉。

SWR 的更多介绍可以看看这篇文章:

谈谈 stale-while-revalidate

这篇文章介绍 swr 这个库,其实想重点介绍里面的一个功能:Suspense mode 。什么意思呢,来看一段官方的代码:

import { Suspense } from 'react'
import useSWR from 'swr'

function Profile() {
  const { data } = useSWR('/api/user', fetcher, { suspense: true })
  return <div>hello, {data.name}</div>
}

function App() {
  return (
    <Suspense fallback={<div>loading...</div>}>
      <Profile/>
    </Suspense>
  )
}

其中 fetcher 是 API 请求的函数封装。useSWR 会把第一个参数作为形参传递给 fetcher ,然后 fetcher 会将请求的结果包裹在 Promise 中返回,下面举个例子:

const fetch = async (params) => {
  // 模拟后台接口响应延迟
  await new Promise(resolve => setTimeout(resolve, 3000));
  return { name: "2333" };
}

当传入的 fetcher 还是 pending 状态时,展示 fallback 中的内容,当状态变为 fullfilled 时渲染子组件。

看到这里可以发现,SWR 的 Suspense mode 和异步组件加载有很多相同之处:

  • 无论 fetcher 还是 import() 都是返回一个 Promise ;
  • 都需要一个函数包裹才能和 React.Suspense 结合使用;

为了搞清楚 Suspense 的机制,其实就是搞清楚 useSWR 里面做了什么。

React.Suspense 原理浅析

原理部分参考了黄子毅大佬的文章,对 API 用法以及一些细节讲得非常详细,推荐看一下:

精读《Hooks 取数 - swr 源码》

这边我们主要关心 Suspense 的实现。Suspense 要求代码 suspended,即要求内部的子组件抛出一个可以被捕获的 Promise 异常,在这个 Promise 结束后再渲染组件。

核心代码就这一段,抛出取数的 Promise:

throw CONCURRENT_PROMISES[key];

等取数完毕后再返回 useSWR API 定义的结构:

return {
  error: latestError,
  data: latestData,
  revalidate,
  isValidating
};

如果没有上面 throw 的一步,在取数完毕前组件就会被渲染出来,所以 throw 了请求的 Promise 使得这个请求函数支持了 Suspense。

总结一下其实就是,useSWR 在数据加载之前先抛出了一个取数的 Promise ,然后 Suspense 内部通过 try...catch 捕获这个 Promise ,然后等待 Promise 状态变为 fullfilled 之后,也就是取数完毕之后,渲染子组件。

那么其实 React.lazy 做的事情也差不多,应该也是抛出一个 Promise (所以文档中提到需要支持 Promise)。然后 Suspense 其实和 ErrorBoundary 很像,不过 Suspense 只负责捕获抛出的 Promise 。

useSWR 使用遇到的问题

看了上面的介绍,useSWR 似乎集众多优点于一身,但是本人使用的时候还是遇到了一点问题:

  • useSWR 必须要传第一个参数,这个参数会作为形参传给 fetch 函数;
  • 如果传空值或者传空字符串,useSWR 不会执行;
  • useSWR 是 hooks ,不能放在函数或者条件判断中;

另外现在项目中都把后端接口的地址和请求方法直接封装进一个函数了,然后调接口的时候直接调用这个函数就行。但是 useSWR 却要在每次请求的时候指定后端接口地址,这样的做法就有点不太合理,实际应用到项目的时候可能需要二次封装。

参考

React Suspense + 自定义Hook开启数据请求新方式
React.Suspence - React 官方文档
swr - npm
谈谈 stale-while-revalidate
精读《Hooks 取数 - swr 源码》

 类似资料: