react-Suspense工作原理分析

Suspense 基本应用

Suspense 目前在 react 中一般配合 lazy 使用,当有一些组件需要动态加载(例如各种插件)时可以利用 lazy 方法来完成。其中 lazy 接受类型为 Promise<() => {default: ReactComponet}> 的参数,并将其包装为 react 组件。ReactComponet 可以是类组件函数组件或其他类型的组件,例如:

 const Lazy = React.lazy(() => import("./LazyComponent"))
 <Suspense fallback={
   "loading"}>
        <Lazy/> // lazy 包装的组件
 </Suspense>

由于 Lazy 往往是从远程加载,在加载完成之前 react 并不知道该如何渲染该组件。此时如果不显示任何内容,则会造成不好的用户体验。因此 Suspense 还有一个强制的参数为 fallback,表示 Lazy 组件加载的过程中应该显示什么内容。往往 fallback 会使用一个加载动画。当加载完成后,Suspense 就会将 fallback 切换为 Lazy 组件的内容。一个完整的例子如下:

function LazyComp(){
   
  console.info("sus", "render lazy")
  return "i am a lazy man"
}

function delay(ms){
   
  return new Promise((resolve, reject) => {
   
    setTimeout(resolve, ms)
  })
}

// 模拟动态加载组件
const Lazy = lazy(() => delay(5000).then(x => ({
   "default": LazyComp})))

function App() {
   
  const context = useContext(Context)
  console.info("outer context")
  return (
      <Suspense fallback={
   "loading"}>
        <Lazy/>
      </Suspense>
  )
}

这段代码定义了一个需要动态加载的 LazyComp 函数式组件。会在一开始显示 fallback 中的内容 loading,5s 后显示 i am a lazy man。

Suspense 原理

虽然说 Suspense 往往会配合 lazy 使用,但是 Suspense 是否只能配合 lazy 使用?lazy 是否又必须配合Suspense? 要搞清楚这两个问题,首先要明白 Suspense 以及 lazy 是在整个过程中扮演的角色,这里先给出一个简单的结论:

  • Suspense: 可以看做是 react 提供用了加载数据的一个标准,当加载到某个组件时,如果该组件本身或者组件需要的数据是未知的,需要动态加载,此时就可以使用 Suspense。Suspense 提供了加载 -> 过渡 -> 完成后切换这样一个标准的业务流程。
  • lazy: lazy 是在 Suspense 的标准下,实现的一个动态加载的组件的工具方法。

从上面的描述即可以看出,Suspense 是一个加载数据的标准,lazy 只是该标准下实现的一个工具方法。那么说明 Suspense 除配合了 lazy 还可以有其他应用场景。而 lazy 是 Suspense 标准下的一个工具方法,因此无法脱离 Suspense 使用。接下来通过 lazy + Suspense 方式来给大家分析具体原理,搞懂了这部分,我们利用 Suspense 实现自己的数据加载也不是难事。

基本流程

在深入了解细节之前,我们先了解一下 lazy + Suspense 的基本原理。这里需要一些 react 渲染流程的基本知识。为了统一,在后续将动态加载的组件称为 primary 组件,fallback 传入的组件称为 fallback 组件,与源码保持一致。

  1. 当 react 在 beginWork 的过程中遇到一个 Suspense 组件时,会首先将 primary 组件作为其子节点,根据 react 的遍历算法,下一个遍历的组件就是未加载完成的 primary 组件。
  2. 当遍历到 primary 组件时,primary 组件会抛出一个异常。该异常内容为组件 promise,react 捕获到异常后,发现其是一个 promise,会将其 then 方法添加一个回调函数,该回调函数的作用是触发 Suspense 组件的更新。并且将下一个需要遍历的元素重新设置为 Suspense,因此在一次 beginWork 中,Suspense 会被访问两次。
  3. 又一次遍历到 Suspense,本次会将 primary 以及 fallback 都生成,并且关系如下:

参考React实战视频讲解:进入学习

react-Suspense工作原理分析_第1张图片 虽然 primary 作为 Suspense 的直接子节点,但是 Suspense 会在 beginWork 阶段直接返回 fallback。使得直接跳过 primary 的遍历。因此此时 primary 必定没有加载完成,所以也没必要再遍历一次。本次渲染结束后,屏幕上会展示 fallback 的内容

  1. 当 primary 组件加载完成后,会触发步骤 2 中 then,使得在 Suspense 上调度一个更新,由于此时加载已经完成,Suspense 会直接渲染加载完成的 primary 组件,并删除 fallback 组件。

这 4 个步骤看起来还是比较复杂。相对于普通的组件主要有两个不同的流程:

  1. primary 会组件抛出异常,react 捕获异常后继续 beginWork 阶段。
  2. 整个 beginWork 节点,Suspense 会被访问两次

不过基本逻辑还是比较简单,即是:

  1. 抛出异常
  2. react 捕获,添加回调
  3. 展示 fallback
  4. 加载完成,执行回调
  5. 展示加载完成后的组件

整个 beginWork 遍历顺序为:

 Suspense -> primary -> Suspense -> fallback

源码解读 - primary 组件

整个 Suspend 的逻辑相对于普通流程实际上是从 primary 组件开始的,因此我们也从 react 是如何处理 primary 组件开始探索。找到 react 在 beginWork 中处理处理 primary 组件的逻辑的方法 mountLazyComponent,这里我摘出一段关键的代码:

  const props = workInProgress.pendingProps;
  const lazyComponent: LazyComponentType<any, any> = elementType;
  const payload = lazyComponent._payload;
  const init = lazyComponent._init;
  let Component = init(payload); // 如果未加载完成,则会抛出异常,否则会返回加载完成的组件

其中最关键的部分莫过于这个 init 方法,执行到这个方法时,如果没有加载完成就会抛出 Promise 的异常。如果加载完成就直接返回完成后的组件。我们可以看到这个 init 方法实际上是挂载到 lazyComponent._init 方法,lazyComponent 则就是 React.lazy() 返回的组件。我们找到 React.lazy() :

export function lazy<T>(
  ctor: () => Thenable<{
   default: T, ...}>,
): LazyComponent<T, Payload<T>> {
   
  const payload: Payload<T> = {
   
    // We use these fields to store the result.
    _status: Uninitialized,
    _result: ctor,
  };

  const lazyType: LazyComponent<T, Payload<T>> = {
   
    $$typeof: REACT_LAZY_TYPE,
    _payload: payload,
    _init: lazyInitializer,
  

你可能感兴趣的:(reactjs)