深入理解React中Suspense與lazy的原理
一、前面的話
在react中為我們提供了一個(gè)非常有用的組件,那就是<Suspense/>
,他可以包裹一個(gè)異步組件,當(dāng)這個(gè)異步組件處于pending
狀態(tài)的時(shí)候會(huì)展示一個(gè)過(guò)渡的UI,當(dāng)異步組件處于resolved
狀態(tài)的時(shí)候會(huì)顯示真正的UI,我們來(lái)看一下如何使用Suspense
和 react提供的lazy
結(jié)合起來(lái)達(dá)到異步加載狀態(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> ) }
它的效果如下:
接下來(lái)我們就來(lái)一步一步看一下這究竟是怎么做到這一點(diǎn)的!
二、lazy懶加載組件
要先從lazy
這個(gè)api開(kāi)始說(shuō)起,根據(jù)上面的內(nèi)容,LazyComponent
是由lazy
這個(gè)調(diào)用返回的結(jié)果,它能夠被直接渲染,在沒(méi)有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
的實(shí)現(xiàn)原理
function lazy(ctor) { var payload = { // 創(chuàng)建一個(gè)payload _status: Uninitialized, // -1 _result: ctor, // ctor 就是用戶傳遞的哪個(gè)()=> import("xxxxx") 實(shí)際上等價(jià)于 ()=> Promise<any> }; var lazyType = { // 這是一個(gè)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ù)我提供的注釋我們可以看到,其實(shí)lazy就是返回了一個(gè)REACT_LAZY_TYPE
類型的ReactElement節(jié)點(diǎn),并且用一個(gè)狀態(tài)機(jī)記錄了當(dāng)前的這個(gè)節(jié)點(diǎn)處于什么樣的狀態(tài),引用者傳進(jìn)來(lái)的函數(shù)引用
這里要重點(diǎn)分析一下()=> import('xxxx')
,import('xxx')
是ES6提供的一種異步加載模塊的方式,他會(huì)返回一個(gè)Promise
,因此可以使用.then
獲取異步加載所得到的數(shù)據(jù)
接下來(lái)我們看一下lazyInitializer
的實(shí)現(xiàn)
function lazyInitializer(payload) { if (payload._status === Uninitialized) { // 如果是初始化狀態(tài) var ctor = payload._result; // ()=> import('xxx') var thenable = ctor(); // 得到一個(gè)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)記正在進(jìn)行 pending._result = thenable; } } if (payload._status === Resolved) { // 如果不是初始化 var moduleObject = payload._result; if (moduleObject === undefined) { 報(bào)錯(cuò) } if (!("default" in moduleObject)) { 報(bào)錯(cuò) } return moduleObject.default; } else { // 初始化都會(huì)進(jìn)入到這里 throw payload._result; // 拋出錯(cuò)誤 } }
經(jīng)過(guò)分析我們會(huì)發(fā)現(xiàn)lazyInitializer
會(huì)根據(jù)payload
的狀態(tài)來(lái)采取不同的行為:
- 如果是初始化狀態(tài) 在這里它會(huì)執(zhí)行用戶傳進(jìn)來(lái)的函數(shù),得到一個(gè)Promise,并且開(kāi)始調(diào)用這個(gè)Promise,得到異步的結(jié)果,并且標(biāo)記自己處于
Pedning
狀態(tài),然后拋出錯(cuò)誤 - 如果
Resolved
的狀態(tài)那么就判斷這個(gè)得到的值是否合法,合法就返回給調(diào)用者
但不用擔(dān)心,此時(shí)我們分析了這個(gè)函數(shù)如果執(zhí)行的話,直到現(xiàn)在用戶只是調(diào)用了lazy
,這個(gè)函數(shù)還沒(méi)到執(zhí)行的時(shí)候,現(xiàn)在用戶僅僅只是得到了一個(gè)lazy
類型的ReactElement
類型的節(jié)點(diǎn)
而真正讓這個(gè)函數(shù)執(zhí)行得地方還是得在render
階段,當(dāng)調(diào)和到lazy
類型的節(jié)點(diǎn)的時(shí)候,會(huì)執(zhí)行mountLazyComponent
function mountLazyComponent( _current, workInProgress, elementType, renderLanes ) { var props = workInProgress.pendingProps; // lazy的組件一般沒(méi)有props var lazyComponent = elementType; // ReactElement var payload = lazyComponent._payload; // 這就是上面的payload var init = lazyComponent._init; // 獲取那個(gè)init函數(shù),就是我們上面分析的那個(gè) var Component = init(payload); // 調(diào)用它,第一次會(huì)拋出錯(cuò)誤 //芭比Q,下面不用看了 ... }
根據(jù)我們上面的分析,在調(diào)用lazyInitializer
函數(shù)的時(shí)候,如果是第一次調(diào)用,會(huì)進(jìn)入第一種情況,狀態(tài)還是初始化的狀態(tài),因此會(huì)執(zhí)行異步函數(shù),得到一個(gè)正在調(diào)用的Promise
,然后會(huì)調(diào)用.then
獲取它的結(jié)果,然后將其保存在payload
中,然后將狀態(tài)置為Pending
,最后拋出錯(cuò)誤,所以后面的邏輯都不用看了,第一次在這里會(huì)拋出錯(cuò)誤,阻塞后面的代碼,整個(gè)render階段被迫提前結(jié)束
如果提前結(jié)束了render階段
,那么后面該如何運(yùn)行呢?
原來(lái)當(dāng)lazy
類型的render過(guò)程中,準(zhǔn)確的來(lái)說(shuō)應(yīng)該是beginWork
中因?yàn)榈谝淮螆?zhí)行init
函數(shù)導(dǎo)致拋出錯(cuò)誤,阻塞了后面的過(guò)程,react會(huì)提前結(jié)束beginWork
環(huán)節(jié),然后react會(huì)捕獲這個(gè)錯(cuò)誤,還記得那個(gè)workLoop
么?它是這樣子的:
do { try { workLoopSync(); // 當(dāng)這里拋出錯(cuò)誤時(shí) break; } catch (thrownValue) { handleError(root, thrownValue); // 會(huì)來(lái)到這里 } } while (true);
因此實(shí)際上react并不會(huì)因?yàn)閽伋隽诉@個(gè)錯(cuò)誤就完蛋了,甚至這個(gè)錯(cuò)誤是刻意拋出的,為的就是在handleError
中捕獲它,然后做不同的邏輯處理
在handleError
中會(huì)基于拋出錯(cuò)誤的節(jié)點(diǎn)開(kāi)始提前進(jìn)入completeWork
,然后將整棵樹(shù)標(biāo)記為未完成的狀態(tài),最后因?yàn)樯蠈雍瘮?shù)拿到這個(gè)是否調(diào)和完整棵樹(shù)的狀態(tài),決定是否進(jìn)行commit
流程
結(jié)果就是這棵樹(shù)沒(méi)有完成,因此不會(huì)進(jìn)行commit
階段,第一次render
因?yàn)?code>lazy類型組件的存在就這樣匆匆結(jié)束了
那現(xiàn)相信大家和我有同樣的問(wèn)題,那react是怎么重啟render
的呢? ,因?yàn)樵谄匠i_(kāi)發(fā)中l(wèi)azy組件也是可以渲染出組件的呀,所以一定有一個(gè)重啟render的過(guò)程才能做到。
原來(lái)在handleError
的過(guò)程中有一個(gè)這樣的過(guò)程,如果發(fā)現(xiàn)了拋出錯(cuò)誤的參數(shù)是一個(gè)Promise
的話,就會(huì)認(rèn)定他是一個(gè)懶加載的情況,然后做出重啟的操作,正巧我們init
拋出錯(cuò)誤的信息剛好是一個(gè)Promise
,而重啟的操作如下:
function throwException(value){// 這個(gè)value就是錯(cuò)誤信息 ... if ( value !== null && typeof value === "object" && typeof value.then === "function" // 如果是一個(gè)Promise ) { var wakeable = value; // var suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); // 如有上層有Suspense包裹的話,這里先不談 if (suspenseBoundary !== null) { ... } else{ attachPingListener(root, wakeable, rootRenderLanes); // 這里就是關(guān)鍵了,它會(huì)監(jiān)聽(tīng)這個(gè)Promise的情況 } }
那么attachPingListener
發(fā)生了什么呢?
簡(jiǎn)化一下就是這樣的
var ping = pingSuspendedRoot.bind(null, root, wakeable, lanes); weakable.then(ping , ping)
看到了嗎,如果這個(gè)Promise
的狀態(tài)一旦從Pending
狀態(tài)變成其他狀態(tài),就會(huì)執(zhí)行這個(gè)pingSuspendedRoot
,它里面就藏著重新發(fā)起調(diào)度的ensureRootIsScheduled
邏輯,然后會(huì)把更新流程重走一遍,從render
到commit
,最終就呈現(xiàn)出了UI。
這里需要注意的一點(diǎn)就是當(dāng)重啟的這一次render
階段其實(shí)也會(huì)遇到lazy
類型的節(jié)點(diǎn),那它還會(huì)拋出錯(cuò)誤嗎?
其實(shí)是不會(huì)的,因?yàn)檫@一次來(lái)到lazy
節(jié)點(diǎn)時(shí),執(zhí)行的init
函數(shù)會(huì)發(fā)現(xiàn)狀態(tài)已經(jīng)被修改為Resolved
的狀態(tài)了, 會(huì)直接返回結(jié)果,然后返回的結(jié)果通常來(lái)說(shuō)是一個(gè)組件,就是異步加載的組件,把它作為子組件再繼續(xù)構(gòu)建fiber樹(shù)
function mountLazyComponent( _current, workInProgress, elementType, renderLanes ) { var props = workInProgress.pendingProps; // lazy的組件一般沒(méi)有props var lazyComponent = elementType; // ReactElement var payload = lazyComponent._payload; // 這就是上面的payload var init = lazyComponent._init; // 獲取那個(gè)init函數(shù),就是我們上面分析的那個(gè) var Component = init(payload); // 這一次調(diào)用直接獲取到值,而不會(huì)拋出錯(cuò)誤,往下調(diào)和異步組件 workInProgress.type = Component; var resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component)); // 獲取對(duì)應(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類型的組件原理我們就分析完了,它其實(shí)利用的是react
強(qiáng)大的異常捕獲機(jī)制,以及Promise
靈敏的狀態(tài)機(jī)來(lái)實(shí)現(xiàn)的,我畫(huà)個(gè)圖給大家總結(jié)一下
三、Suspense原理
當(dāng)我們分析了上面的lazy
類型的組件之后Suspense
就很好學(xué)習(xí)了
Suspense
本質(zhì)上就是一個(gè)ReactElement
類型的對(duì)象,沒(méi)啥好說(shuō)的;關(guān)鍵要看在render
階段react如何處理這種類型的fiber組件的,下面一起來(lái)看一下
初始化
在初始化時(shí)僅僅只是創(chuàng)建了fiber,然后繼續(xù)調(diào)和子組件,由于他的組件就是lazy
類型的組件,因此還是回到上面的邏輯,lazy
組件會(huì)拋錯(cuò)啊,因此第一次render
階段終止了,但是在handleError
處理錯(cuò)誤的時(shí)候,因?yàn)樗?code>Suspense包裹著,因此邏輯會(huì)有不同
function throwException(value){ // 這個(gè)value就是錯(cuò)誤信息 ... if ( value !== null && typeof value === "object" && typeof value.then === "function" // 如果是一個(gè)Promise ) { var wakeable = value; // var suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber); // 如有上層有Suspense包裹的話,這里會(huì)判定有,實(shí)際上就是遍歷祖先節(jié)點(diǎn),看是否有Suspense類型的fiber if (suspenseBoundary !== null) { suspenseBoundary.flags &= ~ForceClientRender; markSuspenseBoundaryShouldCapture( // 打標(biāo)簽應(yīng)該被捕獲 suspenseBoundary, returnFiber, sourceFiber, root, rootRenderLanes ); attachPingListener(root, wakeable, rootRenderLanes); // 監(jiān)聽(tīng)重啟 } else{ ... } }
實(shí)際上這個(gè)邏輯和lazy還是一樣的,就是監(jiān)聽(tīng)Promise
的狀態(tài),在Promise
有結(jié)果的時(shí)候再重啟一次render
,這一點(diǎn)是一致的,通過(guò)這個(gè)機(jī)制可以確保當(dāng)異步組件加載完成后react運(yùn)行時(shí)能夠知道在此時(shí)更新頁(yè)面,呈現(xiàn)出最新的UI
但是我們知道從效果上來(lái)看,在有Suspense
包裹的時(shí)候,在異步組件加載過(guò)程中應(yīng)該會(huì)立馬展示一個(gè)過(guò)渡UI,也就是fallback
對(duì)應(yīng)的參數(shù),而需要做到這一點(diǎn)需要發(fā)起一次調(diào)度啊,也就是說(shuō)需要經(jīng)歷一個(gè)render
+commit
才能做到啊
過(guò)渡fiber節(jié)點(diǎn)
原來(lái)這一切的一切在第一次render的時(shí)候就有準(zhǔn)備了,在第一次構(gòu)建fiber樹(shù)的時(shí)候,假設(shè)我們的組件是下面這樣的
<Suspense fallback={...}> <Lazy/> </Suspense>
那么實(shí)際上在構(gòu)建fiber
樹(shù)的時(shí)候會(huì)有這樣的fiber
結(jié)構(gòu)
因此它并不是每個(gè)組件對(duì)應(yīng)一個(gè)fiber
節(jié)點(diǎn),Suspense
對(duì)應(yīng)的實(shí)際上是有2個(gè)fiber節(jié)點(diǎn),當(dāng)我們知道這一點(diǎn)之后,當(dāng)做了監(jiān)聽(tīng)完的動(dòng)作之后,我們?cè)倩氐酵鈱涌匆幌?,?huì)執(zhí)行一個(gè)completeUnitOfWork
的動(dòng)作,這個(gè)動(dòng)作實(shí)際上在上面我們講到的只有lazy
的情況也會(huì)執(zhí)行,只不過(guò)在只有lazy
組件的時(shí)候它會(huì)一直調(diào)和到root
節(jié)點(diǎn),導(dǎo)致workInProgress
為null
,而在有Suspense
會(huì)表現(xiàn)的有所不同
因?yàn)檫@是由于出現(xiàn)了異常導(dǎo)致的completeUnitOfWork
,因此不會(huì)走正常的completeWork
,而是走unwindWork(current, completedWork);
在unwindWork
向上歸并的時(shí)候,如果遇到有Suspense
節(jié)點(diǎn)的情況會(huì)保留這個(gè)Suspense
節(jié)點(diǎn)的信息,實(shí)際上就是不會(huì)一直往上走到root節(jié)點(diǎn),而是將workInProgress
指向這個(gè)Suspense
的fiber節(jié)點(diǎn),然后就退出completeWork
的流程,然后我們?cè)賮?lái)看一下render
階段的引擎函數(shù)
do { try { workLoopSync(); // 這里面需要workInProgress有值才能正常運(yùn)行 break; } catch (thrownValue) { handleError(root, thrownValue); // 結(jié)束后,還是會(huì)執(zhí)行 } } while (true);
handleError
結(jié)束后還會(huì)繼續(xù)接著render,在上面提到的只有l(wèi)azy組件的情況下,因?yàn)?code>workInProgress不存在所以直接break
退出了render
流程,而在Suspense
組件存在的情況下,會(huì)繼續(xù)從這個(gè)Suspense
開(kāi)始繼續(xù)render
這一次render
就會(huì)直接調(diào)和fallback
的內(nèi)容,這一次根本就不會(huì)遇到lazy
類型的組件了,直到整棵fiber樹(shù)調(diào)和完成,然后接著正常進(jìn)行commit
流程,所以用戶看到的就是帶有fallback
的UI界面
等到異步組件重新加載完成后,會(huì)重新執(zhí)行一次render
+ commit
構(gòu)建出含有異步組件的界面
小結(jié): 以上就是Suspense,主要是react在擁有Suspense
類型的組件的過(guò)程中做了處理,使其多了一次默認(rèn)的render
+ commit
的流程,從而使用戶能夠看到含有過(guò)渡狀態(tài)的UI,我依然用一個(gè)圖來(lái)給大家總結(jié)一下
以上就是深入理解React中Suspense與lazy的原理的詳細(xì)內(nèi)容,更多關(guān)于React Suspense lazy的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Webpack3+React16代碼分割的實(shí)現(xiàn)
這篇文章主要介紹了Webpack3+React16代碼分割的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03React、Vue中key的作用詳解 (key的內(nèi)部原理解析)
key是虛擬DOM對(duì)象的標(biāo)識(shí),當(dāng)狀態(tài)中的數(shù)據(jù)發(fā)生變化時(shí),Vue會(huì)根據(jù)[新數(shù)據(jù)]生成[新的虛擬DOM],本文給大家介紹React、Vue中key的作用詳解 (key的內(nèi)部原理解析),感興趣的朋友一起看看吧2023-10-10React如何使用Portal實(shí)現(xiàn)跨層級(jí)DOM渲染
Portal 就像是一個(gè)“傳送門”,能讓你把組件里的元素“傳送到”其他 DOM 節(jié)點(diǎn)下面去渲染,下面小編就來(lái)和大家簡(jiǎn)單介紹一下具體的使用方法吧2025-04-04React使用Ant Design方式(簡(jiǎn)單使用)
文章介紹了AntDesign組件庫(kù),它是基于AntDesign設(shè)計(jì)體系的ReactUI組件庫(kù),主要用于研發(fā)企業(yè)級(jí)中后臺(tái)產(chǎn)品,文章詳細(xì)講解了如何下載和按需引入antd組件庫(kù),并通過(guò)一個(gè)小案例展示了如何使用antd進(jìn)行布局和改造,最后,文章提醒大家在使用過(guò)程中可以參考官網(wǎng)的屬性介紹2024-11-11React Antd中如何設(shè)置表單只輸入數(shù)字
這篇文章主要介紹了React Antd中如何設(shè)置表單只輸入數(shù)字問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06