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

image

本文介紹與 Suspense 在三種情景下使用方法,并結(jié)合源碼進行相應(yīng)解析。歡迎關(guān)注個人博客

Code Spliting

在 16.6 版本之前,code-spliting 通常是由第三方庫來完成的,比如 react-loadble(核心思路為: 高階組件 + webpack dynamic import), 在 16.6 版本中提供了 Suspenselazy 這兩個鉤子, 因此在之后的版本中便可以使用其來實現(xiàn) Code Spliting

目前階段, 服務(wù)端渲染中的 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 將傳入的參數(shù)封裝成一個 LazyComponent

function lazy(ctor) {
  return {
    $$typeof: REACT_LAZY_TYPE, // 相關(guān)類型
    _ctor: ctor,
    _status: -1,   // dynamic import 的狀態(tài)
    _result: null, // 存放加載文件的資源
  };
}

觀察 readLazyComponentType 后可以發(fā)現(xiàn) dynamic import 本身類似 Promise 的執(zhí)行機制, 也具有 PendingResolvedRejected 三種狀態(tài), 這就比較好理解為什么 LazyComponent 組件需要放在 Suspense 中執(zhí)行了(Suspense 中提供了相關(guān)的捕獲機制, 下文會進行模擬實現(xiàn)`), 相關(guān)源碼如下:

function readLazyComponentType(lazyComponent) {
  const status = lazyComponent._status;
  const result = lazyComponent._result;
  switch (status) {
    case Resolved: { // Resolve 時,呈現(xiàn)相應(yīng)資源
      const Component = result;
      return Component;
    }
    case Rejected: { // Rejected 時,throw 相應(yīng) error
      const error = result;
      throw error;
    }
    case Pending: {  // Pending 時, throw 相應(yīng) thenable
      const thenable = result;
      throw thenable;
    }
    default: { // 第一次執(zhí)行走這里
      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

為了解決獲取的數(shù)據(jù)在不同時刻進行展現(xiàn)的問題(在 suspenseDemo 中有相應(yīng)演示), Suspense 給出了解決方案。

下面放兩段代碼,可以從中直觀地感受在 Suspense 中使用 Async Data Fetching 帶來的便利。

  • 一般進行數(shù)據(jù)獲取的代碼如下:
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 中進行數(shù)據(jù)獲取的代碼如下:
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 中進行數(shù)據(jù)獲取的代碼量相比正常的進行數(shù)據(jù)獲取的代碼少了將近一半!少了哪些地方呢?

  • 減少了 loading 狀態(tài)的維護(在最外層的 Suspense 中統(tǒng)一維護子組件的 loading)
  • 減少了不必要的生命周期的書寫

總結(jié): 如何在 Suspense 中使用 Data Fetching

當(dāng)前 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 中使用第一步創(chuàng)建的 resource:

const data = resource.read('demo')

相關(guān)思路解讀

來看下源碼中 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;
}

結(jié)合該部分源碼, 進行如下推測:

  1. 第一次請求沒有緩存, 子組件 throw 一個 thenable 對象, Suspense 組件內(nèi)的 componentDidCatch 捕獲之, 此時展示 Loading 組件;
  2. 當(dāng) Promise 態(tài)的對象變?yōu)橥瓿蓱B(tài)后, 頁面刷新此時 resource.read() 獲取到相應(yīng)完成態(tài)的值;
  3. 之后如果相同參數(shù)的請求, 則走 LRU 緩存算法, 跳過 Loading 組件返回結(jié)果(緩存算法見后記);

官方作者是說法如下:

image

所以說法大致相同, 下面實現(xiàn)一個簡單版的 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 }
    </>
  }
}

進行如下調(diào)用

<Suspense fallback={<div>loading...</div>}>
  <PromiseThrower />
</Suspense>

let cache = "";
let returnData = cache;
const fetch = () =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve("數(shù)據(jù)加載完畢");
    }, 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

效果調(diào)試可以點擊這里, 在 16.6 版本之后, componentDidCatch 只能捕獲 commit phase 的異常。所以在 16.6 版本之后實現(xiàn)的 <PromiseThrower> 又有一些差異(即將 throw thenable 移到 componentDidMount 中進行)。

ConcurrentMode + Suspense

當(dāng)網(wǎng)速足夠快, 數(shù)據(jù)立馬就獲取到了,此時頁面存在的 Loading 按鈕就顯得有些多余了。(在 suspenseDemo 中有相應(yīng)演示), SuspenseConcurrent Mode 下給出了相應(yīng)的解決方案, 其提供了 maxDuration 參數(shù)。用法如下:

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

該 Demo 的效果為當(dāng)獲取數(shù)據(jù)的時間大于(是否包含等于還沒確認(rèn)) 500 毫秒, 顯示自定義的 <Loading /> 組件, 當(dāng)獲取數(shù)據(jù)的時間小于 500 毫秒, 略過 <Loading> 組件直接展示用戶的數(shù)據(jù)。相關(guān)源碼

需要注意的是 maxDuration 屬性只有在 Concurrent Mode 下才生效, 可參考源碼中的注釋。在 Sync 模式下, maxDuration 始終為 0。

后記: 緩存算法

  • LRU 算法: Least Recently Used 最近最少使用算法(根據(jù)時間);
  • LFU 算法: Least Frequently Used 最近最少使用算法(根據(jù)次數(shù));

漫畫:什么是 LRU 算法

若數(shù)據(jù)的長度限定是 3, 訪問順序為 set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4), 則根據(jù) LRU 算法刪除的是 (3, 3), 根據(jù) LFU 算法刪除的是 (1, 1)

react-cache 采用的是 LRU 算法。

相關(guān)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,153評論 4 61
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,849評論 1 18
  • react剛剛推出的時候,講react優(yōu)勢搜索結(jié)果是幾十頁。 現(xiàn)在,react已經(jīng)慢慢退火,該用用react技術(shù)棧...
    zhoulujun閱讀 5,217評論 0 11
  • JavaScript 中的 this 一直是比較讓人頭疼,也是面試特別容易問及的問題。下面就參照這《你不知道的 J...
    VioletJack閱讀 358評論 0 2
  • 每天都好喜悅,很開心做這件事。搭配營養(yǎng)均衡的食物,兼顧色香味烹飪,運動,拍照,還忍不住和好幾個人分享了。我真的很藏...
    aseeya閱讀 212評論 0 0