React?Fiber?鏈表操作及原理示例詳解
正文
看了React源碼之后相信大家都會(huì)對(duì)Fiber有自己不同的見(jiàn)解,而我對(duì)Fiber最大的見(jiàn)解就是這玩意兒就是個(gè)鏈表。如果把整個(gè)Fiber樹(shù)當(dāng)成一個(gè)整體確實(shí)有點(diǎn)難理解源碼,但是如果把它拆開(kāi)了,將每個(gè)節(jié)點(diǎn)都看成一個(gè)獨(dú)立單元卻能得到一個(gè)很清晰的思路,接下來(lái)我就簡(jiǎn)單幾點(diǎn)講講,我所認(rèn)為的為什么React要用鏈表這種數(shù)據(jù)結(jié)構(gòu)來(lái)構(gòu)建Fiber架構(gòu)
什么是Fiber
可能了解過(guò)React的靚仔就要說(shuō)了,Fiber就是一個(gè)虛擬dom樹(shù);確實(shí)如此,但是16版本之前的React也存在虛擬dom樹(shù),為什么要用Fiber替代呢?
眾所周知(可能有靚仔不知道),16.8之前React還沒(méi)引入Fiber概念,Reconciler(協(xié)調(diào)器) 會(huì)在mount階段與update階段循環(huán)遞歸mountComponent與updateComponent,此時(shí)數(shù)據(jù)存儲(chǔ)在調(diào)用棧當(dāng)中,因?yàn)槭沁f歸執(zhí)行,所以一當(dāng)開(kāi)始便無(wú)法停止直到遞歸執(zhí)行結(jié)束;如果此時(shí)頁(yè)面中的節(jié)點(diǎn)非常多我們要等到遞歸結(jié)束可能要耗費(fèi)大量的時(shí)間,而且在此之間用戶會(huì)覺(jué)得卡頓,這對(duì)用戶來(lái)說(shuō)絕對(duì)稱不上是好的體驗(yàn);
因此在16版本之后React有了異步可中斷更新與雙緩存的概念,也就是我們熟知的同步并發(fā)模式Concurrent模式,那么這些跟Fiber有什么關(guān)系呢?
Fiber節(jié)點(diǎn)React源碼
首先我們來(lái)看一段關(guān)于Fiber節(jié)點(diǎn)的React源碼
function FiberNode(tag, pendingProps, key, mode) { // Instance //靜態(tài)屬性 this.tag = tag;// this.key = key; this.elementType = null;// this.type = null;//類(lèi)型 this.stateNode = null; // Fiber //關(guān)聯(lián)屬性 this.return = null; this.child = null; this.sibling = null this.index = 0; this.ref = null; //工作屬性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; // Effects this.flags = NoFlags; this.subtreeFlags = NoFlags; this.deletions = null; this.lanes = NoLanes; this.childLanes = NoLanes; this.alternate = null; { // Note: The following is done to avoid a v8 performance cliff. // // Initializing the fields below to smis and later updating them with // double values will cause Fibers to end up having separate shapes. // This behavior/bug has something to do with Object.preventExtension(). // Fortunately this only impacts DEV builds. // Unfortunately it makes React unusably slow for some applications. // To work around this, initialize the fields below with doubles. // // Learn more about this here: // https://github.com/facebook/react/issues/14365 // https://bugs.chromium.org/p/v8/issues/detail?id=8538 this.actualDuration = Number.NaN; this.actualStartTime = Number.NaN; this.selfBaseDuration = Number.NaN; this.treeBaseDuration = Number.NaN; // It's okay to replace the initial doubles with smis after initialization. // This won't trigger the performance cliff mentioned above, // and it simplifies other profiler code (including DevTools). this.actualDuration = 0; this.actualStartTime = -1; this.selfBaseDuration = 0; this.treeBaseDuration = 0; } { // This isn't directly used but is handy for debugging internals: this._debugSource = null; this._debugOwner = null; this._debugNeedsRemount = false; this._debugHookTypes = null; if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') { Object.preventExtensions(this); } } }
可以看到在一個(gè)FiberNode當(dāng)中存在很多屬性,我們大體將他們分為三類(lèi):
- 靜態(tài)屬性:保存當(dāng)前Fiber節(jié)點(diǎn)的 標(biāo)簽,類(lèi)型等;
- 關(guān)聯(lián)屬性:用于連接其他Fiber節(jié)點(diǎn)形成Fiber樹(shù);
- 工作屬性:保存當(dāng)前Fiber節(jié)點(diǎn)的動(dòng)態(tài)工作單元;
而多個(gè)Fiber節(jié)點(diǎn)之間正是通過(guò)關(guān)聯(lián)屬性的連接形成一個(gè)Fiber樹(shù);因?yàn)槊恳粋€(gè)Fiber節(jié)點(diǎn)都是相互獨(dú)立的,因此Fiber節(jié)點(diǎn)之間通過(guò)指針指向的方式產(chǎn)生聯(lián)系,return指向的是父級(jí)節(jié)點(diǎn),child指向的是子節(jié)點(diǎn),sibling指向的是兄弟節(jié)點(diǎn);
如下列這段JSX代碼為例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div> </div>
最終該JSX產(chǎn)生的樹(shù)結(jié)構(gòu)為
Fiber樹(shù)的每個(gè)節(jié)點(diǎn)都是相互獨(dú)立的,利用指針指向讓他們關(guān)聯(lián)在一起;那么我們是不是可以說(shuō)Fiber樹(shù)就是一個(gè)鏈表,關(guān)于什么是鏈表,可以參考我這篇博文 《作為前端你是否了解鏈表這種數(shù)據(jù)結(jié)構(gòu)?》
Fiber樹(shù)是鏈表
可能現(xiàn)在就有靚仔要問(wèn)了,為什么React要選用鏈表這種數(shù)據(jù)結(jié)構(gòu)搭建Fiber架構(gòu)?
我是這么考慮的
- 節(jié)點(diǎn)獨(dú)立
- 節(jié)省操作時(shí)間
- 利于雙緩存與異步可中斷更新操作
節(jié)點(diǎn)獨(dú)立
不知道有沒(méi)有靚仔會(huì)說(shuō)React的Fiber架構(gòu)拿父節(jié)點(diǎn)的child存子節(jié)點(diǎn)拿子節(jié)點(diǎn)的return存父節(jié)點(diǎn)怎么就節(jié)點(diǎn)獨(dú)立了呢?這位靚仔貧道建議你再去學(xué)一下一般類(lèi)型和引用類(lèi)型;父節(jié)的child存的是子節(jié)點(diǎn)的內(nèi)存地址,子節(jié)點(diǎn)的return存的是父節(jié)點(diǎn)的內(nèi)存地址,因此并不會(huì)占用太多空間,說(shuō)白了他們只是有一層關(guān)系將節(jié)點(diǎn)綁定在一起,但是這層關(guān)系并不是包含關(guān)系;就比如你女朋友是你女朋友,你是你一樣,你們是情侶關(guān)系,并不是占有關(guān)系(不提倡?。∽杂蓱賽?ài),人格獨(dú)立);
節(jié)省操作時(shí)間與單向操作
如果Fiber樹(shù)并不是鏈表這種數(shù)據(jù)結(jié)構(gòu)而是數(shù)組這種數(shù)據(jù)結(jié)構(gòu)會(huì)怎么樣呢?我們都知道數(shù)組的存儲(chǔ)需要在內(nèi)存中開(kāi)辟一長(zhǎng)串有序的內(nèi)存,如果我把中間的某個(gè)元素刪除,那么后面的所有元素都要向上移動(dòng)一個(gè)存儲(chǔ)空間,如果現(xiàn)在我有1000個(gè)節(jié)點(diǎn),我把第一個(gè)節(jié)點(diǎn)刪了,那么后面的999個(gè)節(jié)點(diǎn)都需要在內(nèi)存空間上向上移動(dòng)一位,這顯然是非常消耗時(shí)間的;但是如果是鏈表的話我們只需要將指針解綁,移動(dòng)到上一位節(jié)點(diǎn)或者下一節(jié)點(diǎn)就能形成一個(gè)新的鏈表,這在時(shí)間上來(lái)說(shuō)是非常有優(yōu)勢(shì)的;因?yàn)槭?節(jié)點(diǎn)間相互獨(dú)立因此我們僅僅只需要對(duì)指針進(jìn)行操作并且它的操作是單向的我們不需要進(jìn)行雙向解綁;
我們繼續(xù)以這段JSX為例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div> </div>
如果此時(shí)我們要將class為div1的節(jié)點(diǎn)刪除fiber是如何操作的?我們用圖來(lái)解釋
由圖所示,我們只需要將App的child指針改為div2,將div2的return指針改為App即可,然后我們便可以對(duì)div1與div3進(jìn)行銷(xiāo)毀;
利于雙緩存與異步可中斷更新操作
異步可中斷更新
我只能說(shuō)React為了給用戶良好的使用感受確實(shí)是下足了功夫,在React16之前React還采取著原始的同步更新,但是在在16之后React推出了concurrent模式也就是同步并發(fā)模式,在concurrent模式下你的mount與update都將成為異步可中斷更新,至于react為什么要推出異步可中斷更新可參考我這篇文章 《重學(xué)React之為什么需要Scheduler》
現(xiàn)在我們用最直觀的瀏覽器反饋來(lái)看一下Concurrent模式與Legacy模式的區(qū)別
我們看看Legacy模式下的Performance的監(jiān)聽(tīng)
可以看到所有的render階段方法都在同一個(gè)Task完成,如果運(yùn)行時(shí)間過(guò)長(zhǎng)將會(huì)造成卡頓;
我們?cè)倏?strong>Concurrent模式下的Performance的監(jiān)聽(tīng)
在concurrent模式下會(huì)react的render階段會(huì)被分為若干個(gè)時(shí)長(zhǎng)為5ms的Task
這一切歸功于Scheduler調(diào)度器的功勞,因?yàn)?6之前的React沒(méi)有Scheduler所以采用的是所以采用的是遞歸的方式將數(shù)據(jù)存儲(chǔ)在調(diào)用棧當(dāng)中,遞歸一旦開(kāi)始便無(wú)法停止,所以后來(lái)有了Scheduler;而采用鏈表這種數(shù)據(jù)結(jié)構(gòu)(Fiber)存儲(chǔ)數(shù)據(jù)卻能很好的中斷遍歷;我們來(lái)看看Concurrent模式下的入口函數(shù)
function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
可以看到當(dāng)shouldYield() 為true時(shí)workLoopConcurrent方法將會(huì)中斷工作,而shouldYield() 對(duì)應(yīng)的正是scheduler是否需要更新調(diào)度的狀態(tài)
雙緩存
雙緩存的概念在座的靚仔應(yīng)該都清楚,React在運(yùn)行時(shí)會(huì)有兩棵Fiber樹(shù) (mount階段只有workInProgress Fiber樹(shù)), 一顆是current Fiber樹(shù),對(duì)應(yīng)當(dāng)前展示的內(nèi)容,一顆是workInProgress Fiber樹(shù)對(duì)應(yīng)的是正在構(gòu)建的Fiber樹(shù),在mount階段的首次創(chuàng)建會(huì)創(chuàng)建一個(gè)fiberRootNode的根節(jié)點(diǎn),fiberRootNode 有一個(gè)current工作單元屬性,來(lái)回指向Fiber樹(shù),當(dāng)workInProgess Fiber樹(shù)構(gòu)建完成之后current就指向workInprogress Fiber樹(shù),此時(shí)workInProgess Fiber樹(shù)變?yōu)?strong>current Fiber樹(shù),而current Fiber樹(shù)將變?yōu)?strong>workInProgess Fiber樹(shù),由于這一切都是在內(nèi)存中進(jìn)行的,所以稱之為雙緩存;
而這一切剛好運(yùn)用了鏈表的靈活指向,不斷形成一個(gè)新的鏈表;
以上就是React Fiber 鏈表操作原理詳解的詳細(xì)內(nèi)容,更多關(guān)于React Fiber 鏈表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
漸進(jìn)式源碼解析React更新流程驅(qū)動(dòng)
這篇文章主要為大家介紹了漸進(jìn)式源碼解析React更新流程驅(qū)動(dòng)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04詳解react-navigation6.x路由庫(kù)的基本使用
最近兩個(gè)項(xiàng)目都用到了React Navigation,所以就研究一下如何使用,本文主要介紹了react-navigation6.x路由庫(kù)的基本使用,感興趣的可以了解一下2021-11-11詳解react的兩種動(dòng)態(tài)改變css樣式的方法
這篇文章主要介紹了詳解react的兩種動(dòng)態(tài)改變css樣式的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04React通過(guò)classnames庫(kù)添加類(lèi)的方法
這篇文章主要介紹了React通過(guò)classnames庫(kù)添加類(lèi),在vue中添加class是一件非常簡(jiǎn)單的事情,你可以通過(guò)傳入一個(gè)對(duì)象, 通過(guò)布爾值決定是否添加類(lèi),本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09解決React報(bào)錯(cuò)Rendered more hooks than during
這篇文章主要為大家介紹了React報(bào)錯(cuò)Rendered more hooks than during the previous render解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12