深入理解React中Suspense與lazy的原理
一、前面的話
在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
邏輯,然后會把更新流程重走一遍,從render
到commit
,最終就呈現(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)致workInProgress
為null
,而在有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、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-10React如何使用Portal實現(xiàn)跨層級DOM渲染
Portal 就像是一個“傳送門”,能讓你把組件里的元素“傳送到”其他 DOM 節(jié)點下面去渲染,下面小編就來和大家簡單介紹一下具體的使用方法吧2025-04-04React Antd中如何設(shè)置表單只輸入數(shù)字
這篇文章主要介紹了React Antd中如何設(shè)置表單只輸入數(shù)字問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06