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

image

本文介绍与 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 中统一维护子组件的 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 组件返回结果(缓存算法见后记);

官方作者是说法如下:

image

所以说法大致相同, 下面实现一个简单版的 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()}</>;
  }
}
image

效果调试可以点击这里, 在 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 算法。

相关资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,809评论 1 18
  • react刚刚推出的时候,讲react优势搜索结果是几十页。 现在,react已经慢慢退火,该用用react技术栈...
    zhoulujun阅读 5,189评论 0 11
  • JavaScript 中的 this 一直是比较让人头疼,也是面试特别容易问及的问题。下面就参照这《你不知道的 J...
    VioletJack阅读 347评论 0 2
  • 每天都好喜悦,很开心做这件事。搭配营养均衡的食物,兼顾色香味烹饪,运动,拍照,还忍不住和好几个人分享了。我真的很藏...
    aseeya阅读 196评论 0 0