React?Fiber?鏈表操作及原理示例詳解
正文
看了React源碼之后相信大家都會對Fiber有自己不同的見解,而我對Fiber最大的見解就是這玩意兒就是個鏈表。如果把整個Fiber樹當(dāng)成一個整體確實有點難理解源碼,但是如果把它拆開了,將每個節(jié)點都看成一個獨立單元卻能得到一個很清晰的思路,接下來我就簡單幾點講講,我所認(rèn)為的為什么React要用鏈表這種數(shù)據(jù)結(jié)構(gòu)來構(gòu)建Fiber架構(gòu)
什么是Fiber
可能了解過React的靚仔就要說了,Fiber就是一個虛擬dom樹;確實如此,但是16版本之前的React也存在虛擬dom樹,為什么要用Fiber替代呢?
眾所周知(可能有靚仔不知道),16.8之前React還沒引入Fiber概念,Reconciler(協(xié)調(diào)器) 會在mount階段與update階段循環(huán)遞歸mountComponent與updateComponent,此時數(shù)據(jù)存儲在調(diào)用棧當(dāng)中,因為是遞歸執(zhí)行,所以一當(dāng)開始便無法停止直到遞歸執(zhí)行結(jié)束;如果此時頁面中的節(jié)點非常多我們要等到遞歸結(jié)束可能要耗費大量的時間,而且在此之間用戶會覺得卡頓,這對用戶來說絕對稱不上是好的體驗;
因此在16版本之后React有了異步可中斷更新與雙緩存的概念,也就是我們熟知的同步并發(fā)模式Concurrent模式,那么這些跟Fiber有什么關(guān)系呢?
Fiber節(jié)點React源碼
首先我們來看一段關(guān)于Fiber節(jié)點的React源碼
function FiberNode(tag, pendingProps, key, mode) { // Instance //靜態(tài)屬性 this.tag = tag;// this.key = key; this.elementType = null;// this.type = null;//類型 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); } } }
可以看到在一個FiberNode當(dāng)中存在很多屬性,我們大體將他們分為三類:
- 靜態(tài)屬性:保存當(dāng)前Fiber節(jié)點的 標(biāo)簽,類型等;
- 關(guān)聯(lián)屬性:用于連接其他Fiber節(jié)點形成Fiber樹;
- 工作屬性:保存當(dāng)前Fiber節(jié)點的動態(tài)工作單元;
而多個Fiber節(jié)點之間正是通過關(guān)聯(lián)屬性的連接形成一個Fiber樹;因為每一個Fiber節(jié)點都是相互獨立的,因此Fiber節(jié)點之間通過指針指向的方式產(chǎn)生聯(lián)系,return指向的是父級節(jié)點,child指向的是子節(jié)點,sibling指向的是兄弟節(jié)點;
如下列這段JSX代碼為例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div> </div>
最終該JSX產(chǎn)生的樹結(jié)構(gòu)為
Fiber樹的每個節(jié)點都是相互獨立的,利用指針指向讓他們關(guān)聯(lián)在一起;那么我們是不是可以說Fiber樹就是一個鏈表,關(guān)于什么是鏈表,可以參考我這篇博文 《作為前端你是否了解鏈表這種數(shù)據(jù)結(jié)構(gòu)?》
Fiber樹是鏈表
可能現(xiàn)在就有靚仔要問了,為什么React要選用鏈表這種數(shù)據(jù)結(jié)構(gòu)搭建Fiber架構(gòu)?
我是這么考慮的
- 節(jié)點獨立
- 節(jié)省操作時間
- 利于雙緩存與異步可中斷更新操作
節(jié)點獨立
不知道有沒有靚仔會說React的Fiber架構(gòu)拿父節(jié)點的child存子節(jié)點拿子節(jié)點的return存父節(jié)點怎么就節(jié)點獨立了呢?這位靚仔貧道建議你再去學(xué)一下一般類型和引用類型;父節(jié)的child存的是子節(jié)點的內(nèi)存地址,子節(jié)點的return存的是父節(jié)點的內(nèi)存地址,因此并不會占用太多空間,說白了他們只是有一層關(guān)系將節(jié)點綁定在一起,但是這層關(guān)系并不是包含關(guān)系;就比如你女朋友是你女朋友,你是你一樣,你們是情侶關(guān)系,并不是占有關(guān)系(不提倡??!自由戀愛,人格獨立);
節(jié)省操作時間與單向操作
如果Fiber樹并不是鏈表這種數(shù)據(jù)結(jié)構(gòu)而是數(shù)組這種數(shù)據(jù)結(jié)構(gòu)會怎么樣呢?我們都知道數(shù)組的存儲需要在內(nèi)存中開辟一長串有序的內(nèi)存,如果我把中間的某個元素刪除,那么后面的所有元素都要向上移動一個存儲空間,如果現(xiàn)在我有1000個節(jié)點,我把第一個節(jié)點刪了,那么后面的999個節(jié)點都需要在內(nèi)存空間上向上移動一位,這顯然是非常消耗時間的;但是如果是鏈表的話我們只需要將指針解綁,移動到上一位節(jié)點或者下一節(jié)點就能形成一個新的鏈表,這在時間上來說是非常有優(yōu)勢的;因為是 節(jié)點間相互獨立因此我們僅僅只需要對指針進行操作并且它的操作是單向的我們不需要進行雙向解綁;
我們繼續(xù)以這段JSX為例
<div className="App"> <div className='div1'> <div className='div2'> </div> </div> <div className='div3'> </div> </div>
如果此時我們要將class為div1的節(jié)點刪除fiber是如何操作的?我們用圖來解釋
由圖所示,我們只需要將App的child指針改為div2,將div2的return指針改為App即可,然后我們便可以對div1與div3進行銷毀;
利于雙緩存與異步可中斷更新操作
異步可中斷更新
我只能說React為了給用戶良好的使用感受確實是下足了功夫,在React16之前React還采取著原始的同步更新,但是在在16之后React推出了concurrent模式也就是同步并發(fā)模式,在concurrent模式下你的mount與update都將成為異步可中斷更新,至于react為什么要推出異步可中斷更新可參考我這篇文章 《重學(xué)React之為什么需要Scheduler》
現(xiàn)在我們用最直觀的瀏覽器反饋來看一下Concurrent模式與Legacy模式的區(qū)別
我們看看Legacy模式下的Performance的監(jiān)聽
可以看到所有的render階段方法都在同一個Task完成,如果運行時間過長將會造成卡頓;
我們再看Concurrent模式下的Performance的監(jiān)聽
在concurrent模式下會react的render階段會被分為若干個時長為5ms的Task
這一切歸功于Scheduler調(diào)度器的功勞,因為16之前的React沒有Scheduler所以采用的是所以采用的是遞歸的方式將數(shù)據(jù)存儲在調(diào)用棧當(dāng)中,遞歸一旦開始便無法停止,所以后來有了Scheduler;而采用鏈表這種數(shù)據(jù)結(jié)構(gòu)(Fiber)存儲數(shù)據(jù)卻能很好的中斷遍歷;我們來看看Concurrent模式下的入口函數(shù)
function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
可以看到當(dāng)shouldYield() 為true時workLoopConcurrent方法將會中斷工作,而shouldYield() 對應(yīng)的正是scheduler是否需要更新調(diào)度的狀態(tài)
雙緩存
雙緩存的概念在座的靚仔應(yīng)該都清楚,React在運行時會有兩棵Fiber樹 (mount階段只有workInProgress Fiber樹), 一顆是current Fiber樹,對應(yīng)當(dāng)前展示的內(nèi)容,一顆是workInProgress Fiber樹對應(yīng)的是正在構(gòu)建的Fiber樹,在mount階段的首次創(chuàng)建會創(chuàng)建一個fiberRootNode的根節(jié)點,fiberRootNode 有一個current工作單元屬性,來回指向Fiber樹,當(dāng)workInProgess Fiber樹構(gòu)建完成之后current就指向workInprogress Fiber樹,此時workInProgess Fiber樹變?yōu)?strong>current Fiber樹,而current Fiber樹將變?yōu)?strong>workInProgess Fiber樹,由于這一切都是在內(nèi)存中進行的,所以稱之為雙緩存;
而這一切剛好運用了鏈表的靈活指向,不斷形成一個新的鏈表;
以上就是React Fiber 鏈表操作原理詳解的詳細內(nèi)容,更多關(guān)于React Fiber 鏈表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決React報錯Rendered more hooks than during
這篇文章主要為大家介紹了React報錯Rendered more hooks than during the previous render解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12