深度理解 React Suspense(附源码解析)

栏目: IOS · Android · 发布时间: 5年前

内容简介:本文介绍与在 16.6 版本之前,

深度理解 React Suspense(附源码解析)

本文介绍与 Suspense 在三种情景下使用方法,并结合源码进行相应解析。欢迎关注 个人博客

Code Spliting

在 16.6 版本之前, code-spliting 通常是由第三方库来完成的,比如 react-loadble (核心思路为: 高阶组件 + webpack dynamic import), 在 16.6 版本中提供了 Suspenselazy 这两个钩子, 因此在之后的版本中便可以使用其来实现 Code Spliting

目前阶段, 服务端渲染中的 code-spliting 还是得使用 react-loadable , 可查阅 React.lazy , 暂时先不探讨原因。

Code SplitingReact 中的使用方法是在 Suspense 组件中使用 <LazyComponent> 组件:

import { Suspense, lazy } from 'react'

const DemoA = lazy(() => import('./demo/a'))
const DemoB = lazy(() => import('./demo/b'))

<Suspense>
  <NavLink to="/demoA">DemoA</NavLink>
  <NavLink to="/demoB">DemoB</NavLink>

  <Router>
    <DemoA path="/demoA" />
    <DemoB path="/demoB" />
  </Router>
</Suspense>

源码中 lazy 将传入的参数封装成一个 LazyComponent

function lazy(ctor) {
  return {
    $$typeof: REACT_LAZY_TYPE, // 相关类型
    _ctor: ctor,
    _status: -1,   // dynamic import 的状态
    _result: null, // 存放加载文件的资源
  };
}

观察 readLazyComponentType 后可以发现 dynamic import 本身类似 Promise 的执行机制, 也具有 PendingResolvedRejected 三种状态, 这就比较好理解为什么 LazyComponent 组件需要放在 Suspense 中执行了( Suspense 中提供了相关的捕获机制, 下文会进行模拟实现`), 相关源码如下:

function readLazyComponentType(lazyComponent) {
  const status = lazyComponent._status;
  const result = lazyComponent._result;
  switch (status) {
    case Resolved: { // Resolve 时,呈现相应资源
      const Component = result;
      return Component;
    }
    case Rejected: { // Rejected 时,throw 相应 error
      const error = result;
      throw error;
    }
    case Pending: {  // Pending 时, throw 相应 thenable
      const thenable = result;
      throw thenable;
    }
    default: { // 第一次执行走这里
      lazyComponent._status = Pending;
      const ctor = lazyComponent._ctor;
      const thenable = ctor(); // 可以看到和 Promise 类似的机制
      thenable.then(
        moduleObject => {
          if (lazyComponent._status === Pending) {
            const defaultExport = moduleObject.default;
            lazyComponent._status = Resolved;
            lazyComponent._result = defaultExport;
          }
        },
        error => {
          if (lazyComponent._status === Pending) {
            lazyComponent._status = Rejected;
            lazyComponent._result = error;
          }
        },
      );
      // Handle synchronous thenables.
      switch (lazyComponent._status) {
        case Resolved:
          return lazyComponent._result;
        case Rejected:
          throw lazyComponent._result;
      }
      lazyComponent._result = thenable;
      throw thenable;
    }
  }
}

Async Data Fetching

为了解决获取的数据在不同时刻进行展现的问题(在 suspenseDemo 中有相应演示), Suspense 给出了解决方案。

下面放两段代码,可以从中直观地感受在 Suspense 中使用 Async Data Fetching 带来的便利。

  • 一般进行数据获取的代码如下:
export default class Demo extends Component {
  state = {
    data: null,
  };

  componentDidMount() {
    fetchAPI(`/api/demo/${this.props.id}`).then((data) => {
      this.setState({ data });
    });
  }

  render() {
    const { data } = this.state;

    if (data == null) {
      return <Spinner />;
    }

    const { name } = data;

    return (
      <div>{name}</div>
    );
  }
}
  • Suspense 中进行数据获取的代码如下:
const resource = unstable_createResource((id) => {
  return fetchAPI(`/api/demo`)
})

function Demo {
  render() {
    const data = resource.read(this.props.id)

    const { name } = data;

    return (
      <div>{name}</div>
    );
  }
}

可以看到在 Suspense 中进行数据获取的代码量相比正常的进行数据获取的代码少了将近一半!少了哪些地方呢?

loading

总结: 如何在 Suspense 中使用 Data Fetching

当前 Suspense 的使用分为三个部分:

第一步: 用 Suspens 组件包裹子组件

import { Suspense } from 'react'

<Suspense fallback={<Loading />}>
  <ChildComponent>
</Suspense>

第二步: 在子组件中使用 unstable_createResource :

import { unstable_createResource } from 'react-cache'

const resource = unstable_createResource((id) => {
  return fetch(`/demo/${id}`)
})

第三步: 在 Component 中使用第一步创建的 resource :

const data = resource.read('demo')

相关思路解读

来看下源码中 unstable_createResource 的部分会比较清晰:

export function unstable_createResource(fetch, maybeHashInput) {
  const resource = {
    read(input) {
      ...
      const result = accessResult(resource, fetch, input, key);
      switch (result.status) {
        case Pending: {
          const suspender = result.value;
          throw suspender;
        }
        case Resolved: {
          const value = result.value;
          return value;
        }
        case Rejected: {
          const error = result.value;
          throw error;
        }
        default:
          // Should be unreachable
          return (undefined: any);
      }
    },
  };
  return resource;
}

结合该部分源码, 进行如下推测:

  1. 第一次请求没有缓存, 子组件 throw 一个 thenable 对象, Suspense 组件内的 componentDidCatch 捕获之, 此时展示 Loading 组件;
  2. Promise 态的对象变为完成态后, 页面刷新此时 resource.read() 获取到相应完成态的值;
  3. 之后如果相同参数的请求, 则走 LRU 缓存算法, 跳过 Loading 组件返回结果(缓存算法见后记);

官方作者是说法如下:

深度理解 React Suspense(附源码解析)

所以说法大致相同, 下面实现一个简单版的 Suspense :

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 fallback={<div>loading...</div>}>
  <PromiseThrower />
</Suspense>

let cache = "";
let returnData = cache;
const fetch = () =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve("数据加载完毕");
    }, 2000);
  });

class PromiseThrower extends React.Component {
  getData = () => {
    const getData = fetch();

    getData.then(data => {
      returnData = data;
    });
    if (returnData === cache) {
      throw getData;
    }
    return returnData;
  };

  render() {
    return <>{this.getData()}</>;
  }
}

深度理解 React Suspense(附源码解析)

效果调试可以点击 这里 , 在 16.6 版本之后, componentDidCatch 只能捕获 commit phase 的异常。所以在 16.6 版本之后实现的 <PromiseThrower> 又有一些差异(即将 throw thenable 移到 componentDidMount 中进行)。

ConcurrentMode + Suspense

当网速足够快, 数据立马就获取到了,此时页面存在的 Loading 按钮就显得有些多余了。(在 suspenseDemo 中有相应演示), SuspenseConcurrent Mode 下给出了相应的解决方案, 其提供了 maxDuration 参数。用法如下:

<Suspense maxDuration={500} fallback={<Loading />}>
  ...
</Suspense>

该 Demo 的效果为当获取数据的时间大于(是否包含等于还没确认) 500 毫秒, 显示自定义的 <Loading /> 组件, 当获取数据的时间小于 500 毫秒, 略过 <Loading> 组件直接展示用户的数据。 相关源码

需要注意的是 maxDuration 属性只有在 Concurrent Mode 下才生效, 可参考 源码中的注释 。在 Sync 模式下, maxDuration 始终为 0。

后记: 缓存算法

  • LRU 算法: Least Recently Used 最近最少使用算法(根据时间);
  • LFU 算法: Least Frequently Used 最近最少使用算法(根据次数);

漫画:什么是 LRU 算法

若数据的长度限定是 3, 访问顺序为 set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4) , 则根据 LRU 算法删除的是 (3, 3) , 根据 LFU 算法删除的是 (1, 1)

react-cache 采用的是 LRU 算法。

相关资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Go语言学习笔记

Go语言学习笔记

雨痕 / 电子工业出版社 / 2016-6 / 89

作为时下流行的一种系统编程语言,Go 简单易学,性能很好,且支持各类主流平台。已有大量项目采用 Go 编写,这其中就包括 Docker 等明星作品,其开发和执行效率早已被证明。本书经四年多逐步完善,内容覆盖了语言、运行时、性能优化、工具链等各层面知识。且内容经大量读者反馈和校对,没有明显的缺陷和错误。上卷细致解析了语言规范相关细节,便于读者深入理解语言相关功能的使用方法和注意事项。下卷则对运行时源......一起来看看 《Go语言学习笔记》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具