欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解React中Suspense與lazy的原理

 更新時間:2024年04月22日 09:42:46   作者:Story  
在react中為我們提供了一個非常有用的組件,那就是Suspense,本文主要介紹了如何使用Suspense?和?react提供的lazy結(jié)合起來達到異步加載狀態(tài)的目的,感興趣的可以了解下

一、前面的話

在react中為我們提供了一個非常有用的組件,那就是<Suspense/>,他可以包裹一個異步組件,當(dāng)這個異步組件處于pending狀態(tài)的時候會展示一個過渡的UI,當(dāng)異步組件處于resolved狀態(tài)的時候會顯示真正的UI,我們來看一下如何使用Suspense 和 react提供的lazy結(jié)合起來達到異步加載狀態(tài)的目的

import  { lazy , Suspense } from 'react';

const LazyComponent = React.lazy(() => import('./xxx'));

export default function App() {
  return (
    <Suspense fallback={<span>loading...</span>}>
      <LazyComponent/>
    </Suspense>
  )
}

它的效果如下:

接下來我們就來一步一步看一下這究竟是怎么做到這一點的!

二、lazy懶加載組件

要先從lazy這個api開始說起,根據(jù)上面的內(nèi)容,LazyComponent是由lazy這個調(diào)用返回的結(jié)果,它能夠被直接渲染,在沒有Suspense加持的情況下,也是可以異步渲染出組件的,如下所示

const LazyComponent = React.lazy(() => import('./LazyComponent.js'));

const FunctionComponent = () => {
    const [count, setCount] = React.useState(1);

    const onClick = () => {
      setCount(count + 1);
    };


    return (
      <div>
        <button onClick={onClick}>{ count }</button>
        <LazyComponent/> 
      </div>
    );
};

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<FunctionComponent />);

效果如下:

我們看一下lazy的實現(xiàn)原理

function lazy(ctor) {
    var payload = { // 創(chuàng)建一個payload
      _status: Uninitialized, // -1
      _result: ctor, // ctor 就是用戶傳遞的哪個()=> import("xxxxx") 實際上等價于 ()=> Promise<any>
    };
    var lazyType = { // 這是一個REACT_LAZY_TYPE類型的ReactElement
      $$typeof: REACT_LAZY_TYPE,
      _payload: payload,
      _init: lazyInitializer, // 下面分析一下lazyInitializer
    };
    // 下面是給lazyType做屬性的配置,不重要了解即可
    {
      Object.defineProperties(lazyType, {
        defaultProps: {
          configurable: true,
          get: function () {...},
          set: function (newDefaultProps) { ...},
        },
        propTypes: {
          configurable: true,
          get: function () {... },
          set: function (newPropTypes) {...}
        }
      });
    }

    return lazyType;
}

根據(jù)我提供的注釋我們可以看到,其實lazy就是返回了一個REACT_LAZY_TYPE類型的ReactElement節(jié)點,并且用一個狀態(tài)機記錄了當(dāng)前的這個節(jié)點處于什么樣的狀態(tài),引用者傳進來的函數(shù)引用

這里要重點分析一下()=> import('xxxx'),import('xxx')是ES6提供的一種異步加載模塊的方式,他會返回一個Promise,因此可以使用.then獲取異步加載所得到的數(shù)據(jù)

接下來我們看一下lazyInitializer的實現(xiàn)

function lazyInitializer(payload) {
    if (payload._status === Uninitialized) { // 如果是初始化狀態(tài)
      var ctor = payload._result; // ()=> import('xxx')
      var thenable = ctor(); // 得到一個Promise
      thenable.then( // 調(diào)用.then
        function (moduleObject) {
          if (
            payload._status === Pending ||
            payload._status === Uninitialized
          ) { // 標(biāo)記成功
            var resolved = payload;
            resolved._status = Resolved;
            resolved._result = moduleObject;
          }
        },
        function (error) {
          if ( // 標(biāo)記失敗
            payload._status === Pending ||
            payload._status === Uninitialized
          ) {
            var rejected = payload;
            rejected._status = Rejected;
            rejected._result = error;
          }
        }
      );

      if (payload._status === Uninitialized) {// 如果是初始化
        var pending = payload;
        pending._status = Pending; // 標(biāo)記正在進行
        pending._result = thenable;
      }
    }

    if (payload._status === Resolved) { // 如果不是初始化
      var moduleObject = payload._result;
      if (moduleObject === undefined) {
         報錯
      }
      if (!("default" in moduleObject)) {
        報錯
      }
      return moduleObject.default;
    } else {
      // 初始化都會進入到這里
      throw payload._result; // 拋出錯誤
    }
}

經(jīng)過分析我們會發(fā)現(xiàn)lazyInitializer會根據(jù)payload的狀態(tài)來采取不同的行為:

  • 如果是初始化狀態(tài) 在這里它會執(zhí)行用戶傳進來的函數(shù),得到一個Promise,并且開始調(diào)用這個Promise,得到異步的結(jié)果,并且標(biāo)記自己處于Pedning狀態(tài),然后拋出錯誤
  • 如果Resolved的狀態(tài)那么就判斷這個得到的值是否合法,合法就返回給調(diào)用者

但不用擔(dān)心,此時我們分析了這個函數(shù)如果執(zhí)行的話,直到現(xiàn)在用戶只是調(diào)用了lazy,這個函數(shù)還沒到執(zhí)行的時候,現(xiàn)在用戶僅僅只是得到了一個lazy類型的ReactElement類型的節(jié)點

而真正讓這個函數(shù)執(zhí)行得地方還是得在render階段,當(dāng)調(diào)和到lazy類型的節(jié)點的時候,會執(zhí)行mountLazyComponent

function mountLazyComponent(
    _current,
    workInProgress,
    elementType,
    renderLanes
  ) {
    var props = workInProgress.pendingProps; // lazy的組件一般沒有props
    var lazyComponent = elementType; // ReactElement
    var payload = lazyComponent._payload; // 這就是上面的payload
    var init = lazyComponent._init; // 獲取那個init函數(shù),就是我們上面分析的那個
    var Component = init(payload); // 調(diào)用它,第一次會拋出錯誤
    //芭比Q,下面不用看了
    ...
    
}

根據(jù)我們上面的分析,在調(diào)用lazyInitializer函數(shù)的時候,如果是第一次調(diào)用,會進入第一種情況,狀態(tài)還是初始化的狀態(tài),因此會執(zhí)行異步函數(shù),得到一個正在調(diào)用的Promise,然后會調(diào)用.then獲取它的結(jié)果,然后將其保存在payload中,然后將狀態(tài)置為Pending,最后拋出錯誤,所以后面的邏輯都不用看了,第一次在這里會拋出錯誤,阻塞后面的代碼,整個render階段被迫提前結(jié)束

如果提前結(jié)束了render階段,那么后面該如何運行呢?

原來當(dāng)lazy類型的render過程中,準(zhǔn)確的來說應(yīng)該是beginWork中因為第一次執(zhí)行init函數(shù)導(dǎo)致拋出錯誤,阻塞了后面的過程,react會提前結(jié)束beginWork環(huán)節(jié),然后react會捕獲這個錯誤,還記得那個workLoop么?它是這樣子的:

do {
  try {
    workLoopSync(); // 當(dāng)這里拋出錯誤時
    break;
  } catch (thrownValue) {
    handleError(root, thrownValue); // 會來到這里
  }
} while (true);

因此實際上react并不會因為拋出了這個錯誤就完蛋了,甚至這個錯誤是刻意拋出的,為的就是在handleError中捕獲它,然后做不同的邏輯處理

handleError中會基于拋出錯誤的節(jié)點開始提前進入completeWork,然后將整棵樹標(biāo)記為未完成的狀態(tài),最后因為上層函數(shù)拿到這個是否調(diào)和完整棵樹的狀態(tài),決定是否進行commit流程

結(jié)果就是這棵樹沒有完成,因此不會進行commit階段,第一次render因為lazy類型組件的存在就這樣匆匆結(jié)束了

那現(xiàn)相信大家和我有同樣的問題,那react是怎么重啟render的呢? ,因為在平常開發(fā)中l(wèi)azy組件也是可以渲染出組件的呀,所以一定有一個重啟render的過程才能做到。

原來在handleError的過程中有一個這樣的過程,如果發(fā)現(xiàn)了拋出錯誤的參數(shù)是一個Promise的話,就會認定他是一個懶加載的情況,然后做出重啟的操作,正巧我們init拋出錯誤的信息剛好是一個Promise,而重啟的操作如下:

function throwException(value){// 這個value就是錯誤信息
  ...
  if (
      value !== null &&
      typeof value === "object" &&
      typeof value.then === "function" // 如果是一個Promise
    ) {
      var wakeable = value; //
      var suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); // 如有上層有Suspense包裹的話,這里先不談
      if (suspenseBoundary !== null) {
        ...
      } else{
        attachPingListener(root, wakeable, rootRenderLanes); // 這里就是關(guān)鍵了,它會監(jiān)聽這個Promise的情況
      }
}

那么attachPingListener發(fā)生了什么呢?

簡化一下就是這樣的

var ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
weakable.then(ping , ping)

看到了嗎,如果這個Promise的狀態(tài)一旦從Pending狀態(tài)變成其他狀態(tài),就會執(zhí)行這個pingSuspendedRoot,它里面就藏著重新發(fā)起調(diào)度的ensureRootIsScheduled邏輯,然后會把更新流程重走一遍,從rendercommit,最終就呈現(xiàn)出了UI。

這里需要注意的一點就是當(dāng)重啟的這一次render階段其實也會遇到lazy類型的節(jié)點,那它還會拋出錯誤嗎?

其實是不會的,因為這一次來到lazy節(jié)點時,執(zhí)行的init函數(shù)會發(fā)現(xiàn)狀態(tài)已經(jīng)被修改為Resolved的狀態(tài)了, 會直接返回結(jié)果,然后返回的結(jié)果通常來說是一個組件,就是異步加載的組件,把它作為子組件再繼續(xù)構(gòu)建fiber樹

function mountLazyComponent(
    _current,
    workInProgress,
    elementType,
    renderLanes
  ) {
    var props = workInProgress.pendingProps; // lazy的組件一般沒有props
    var lazyComponent = elementType; // ReactElement
    var payload = lazyComponent._payload; // 這就是上面的payload
    var init = lazyComponent._init; // 獲取那個init函數(shù),就是我們上面分析的那個
    var Component = init(payload); // 這一次調(diào)用直接獲取到值,而不會拋出錯誤,往下調(diào)和異步組件
    
    workInProgress.type = Component;
    var resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); // 獲取對應(yīng)的fiber類型
    var resolvedProps = resolveDefaultProps(Component, props);
    var child;

    switch (resolvedTag) {
      case FunctionComponent: {
        ...
        child = updateFunctionComponent( // 繼續(xù)調(diào)和
          null,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes
        );
        return child;
      }
      ...
    }
    
}

至此lazy類型的組件原理我們就分析完了,它其實利用的是react強大的異常捕獲機制,以及Promise靈敏的狀態(tài)機來實現(xiàn)的,我畫個圖給大家總結(jié)一下

三、Suspense原理

當(dāng)我們分析了上面的lazy類型的組件之后Suspense就很好學(xué)習(xí)了

Suspense本質(zhì)上就是一個ReactElement類型的對象,沒啥好說的;關(guān)鍵要看在render階段react如何處理這種類型的fiber組件的,下面一起來看一下

初始化

在初始化時僅僅只是創(chuàng)建了fiber,然后繼續(xù)調(diào)和子組件,由于他的組件就是lazy類型的組件,因此還是回到上面的邏輯,lazy組件會拋錯啊,因此第一次render階段終止了,但是在handleError處理錯誤的時候,因為它被Suspense包裹著,因此邏輯會有不同

function throwException(value){ // 這個value就是錯誤信息
  ...
  if (
      value !== null &&
      typeof value === "object" &&
      typeof value.then === "function" // 如果是一個Promise
    ) {
      var wakeable = value; //
      var suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); // 如有上層有Suspense包裹的話,這里會判定有,實際上就是遍歷祖先節(jié)點,看是否有Suspense類型的fiber
      if (suspenseBoundary !== null) {
        suspenseBoundary.flags &= ~ForceClientRender;
        markSuspenseBoundaryShouldCapture( // 打標(biāo)簽應(yīng)該被捕獲
          suspenseBoundary,
          returnFiber,
          sourceFiber,
          root,
          rootRenderLanes
        ); 
        attachPingListener(root, wakeable, rootRenderLanes); // 監(jiān)聽重啟
      } else{
        ...
      }
}

實際上這個邏輯和lazy還是一樣的,就是監(jiān)聽Promise的狀態(tài),在Promise有結(jié)果的時候再重啟一次render,這一點是一致的,通過這個機制可以確保當(dāng)異步組件加載完成后react運行時能夠知道在此時更新頁面,呈現(xiàn)出最新的UI

但是我們知道從效果上來看,在有Suspense包裹的時候,在異步組件加載過程中應(yīng)該會立馬展示一個過渡UI,也就是fallback對應(yīng)的參數(shù),而需要做到這一點需要發(fā)起一次調(diào)度啊,也就是說需要經(jīng)歷一個render+commit才能做到啊

過渡fiber節(jié)點

原來這一切的一切在第一次render的時候就有準(zhǔn)備了,在第一次構(gòu)建fiber樹的時候,假設(shè)我們的組件是下面這樣的

<Suspense fallback={...}>
  <Lazy/>
</Suspense>

那么實際上在構(gòu)建fiber樹的時候會有這樣的fiber結(jié)構(gòu)

因此它并不是每個組件對應(yīng)一個fiber節(jié)點,Suspense對應(yīng)的實際上是有2個fiber節(jié)點,當(dāng)我們知道這一點之后,當(dāng)做了監(jiān)聽完的動作之后,我們再回到外層看一下,會執(zhí)行一個completeUnitOfWork的動作,這個動作實際上在上面我們講到的只有lazy的情況也會執(zhí)行,只不過在只有lazy組件的時候它會一直調(diào)和到root節(jié)點,導(dǎo)致workInProgressnull,而在有Suspense會表現(xiàn)的有所不同

因為這是由于出現(xiàn)了異常導(dǎo)致的completeUnitOfWork,因此不會走正常的completeWork,而是走unwindWork(current, completedWork);

unwindWork向上歸并的時候,如果遇到有Suspense節(jié)點的情況會保留這個Suspense節(jié)點的信息,實際上就是不會一直往上走到root節(jié)點,而是將workInProgress指向這個Suspense的fiber節(jié)點,然后就退出completeWork的流程,然后我們再來看一下render階段的引擎函數(shù)

do {
  try {
    workLoopSync(); // 這里面需要workInProgress有值才能正常運行
    break;
  } catch (thrownValue) {
    handleError(root, thrownValue);
    // 結(jié)束后,還是會執(zhí)行
  }
} while (true);

handleError結(jié)束后還會繼續(xù)接著render,在上面提到的只有l(wèi)azy組件的情況下,因為workInProgress不存在所以直接break退出了render流程,而在Suspense組件存在的情況下,會繼續(xù)從這個Suspense開始繼續(xù)render

這一次render就會直接調(diào)和fallback的內(nèi)容,這一次根本就不會遇到lazy類型的組件了,直到整棵fiber樹調(diào)和完成,然后接著正常進行commit流程,所以用戶看到的就是帶有fallback的UI界面

等到異步組件重新加載完成后,會重新執(zhí)行一次render + commit 構(gòu)建出含有異步組件的界面

小結(jié): 以上就是Suspense,主要是react在擁有Suspense類型的組件的過程中做了處理,使其多了一次默認的render + commit的流程,從而使用戶能夠看到含有過渡狀態(tài)的UI,我依然用一個圖來給大家總結(jié)一下

以上就是深入理解React中Suspense與lazy的原理的詳細內(nèi)容,更多關(guān)于React Suspense lazy的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • React中使用外部樣式的3種方式(小結(jié))

    React中使用外部樣式的3種方式(小結(jié))

    這篇文章主要介紹了React中使用外部樣式的3種方式(小結(jié)),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • Webpack3+React16代碼分割的實現(xiàn)

    Webpack3+React16代碼分割的實現(xiàn)

    這篇文章主要介紹了Webpack3+React16代碼分割的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-03-03
  • React、Vue中key的作用詳解 (key的內(nèi)部原理解析)

    React、Vue中key的作用詳解 (key的內(nèi)部原理解析)

    key是虛擬DOM對象的標(biāo)識,當(dāng)狀態(tài)中的數(shù)據(jù)發(fā)生變化時,Vue會根據(jù)[新數(shù)據(jù)]生成[新的虛擬DOM],本文給大家介紹React、Vue中key的作用詳解 (key的內(nèi)部原理解析),感興趣的朋友一起看看吧
    2023-10-10
  • Vite搭建React項目的方法步驟

    Vite搭建React項目的方法步驟

    這篇文章主要介紹了Vite搭建React項目的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • React如何使用Portal實現(xiàn)跨層級DOM渲染

    React如何使用Portal實現(xiàn)跨層級DOM渲染

    Portal 就像是一個“傳送門”,能讓你把組件里的元素“傳送到”其他 DOM 節(jié)點下面去渲染,下面小編就來和大家簡單介紹一下具體的使用方法吧
    2025-04-04
  • React使用Ant Design方式(簡單使用)

    React使用Ant Design方式(簡單使用)

    文章介紹了AntDesign組件庫,它是基于AntDesign設(shè)計體系的ReactUI組件庫,主要用于研發(fā)企業(yè)級中后臺產(chǎn)品,文章詳細講解了如何下載和按需引入antd組件庫,并通過一個小案例展示了如何使用antd進行布局和改造,最后,文章提醒大家在使用過程中可以參考官網(wǎng)的屬性介紹
    2024-11-11
  • React Antd中如何設(shè)置表單只輸入數(shù)字

    React Antd中如何設(shè)置表單只輸入數(shù)字

    這篇文章主要介紹了React Antd中如何設(shè)置表單只輸入數(shù)字問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • React Native之TextInput組件解析示例

    React Native之TextInput組件解析示例

    本篇文章主要介紹了React Native之TextInput組件解析示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-08-08
  • 使用React制作一個貪吃蛇游戲的代碼詳解

    使用React制作一個貪吃蛇游戲的代碼詳解

    Snake?Game?使用?ReactJS?項目實現(xiàn)功能組件并相應(yīng)地管理狀態(tài),開發(fā)的游戲允許用戶使用箭頭鍵控制蛇或觸摸屏幕上顯示的按鈕來收集食物并增長長度,本文給大家詳細講解了如何使用?React?制作一個貪吃蛇游戲,需要的朋友可以參考下
    2023-11-11
  • Unity?RectTransform詳解

    Unity?RectTransform詳解

    unity中的ui元素是有嚴(yán)格的父子關(guān)系的,子物體的位置是根據(jù)父物體的變化而變化的,而子物體和父物體聯(lián)系的橋梁就是Anchor,本文重點介紹Unity?RectTransform的相關(guān)知識,感興趣的朋友一起看看吧
    2024-01-01

最新評論