React Fiber 鏈表操作及原理示例詳解

正文

看瞭React源碼之後相信大傢都會對Fiber有自己不同的見解,而我對Fiber最大的見解就是這玩意兒就是個鏈表。如果把整個Fiber樹當成一個整體確實有點難理解源碼,但是如果把它拆開瞭,將每個節點都看成一個獨立單元卻能得到一個很清晰的思路,接下來我就簡單幾點講講,我所認為的為什麼React要用鏈表這種數據結構來構建Fiber架構

什麼是Fiber

可能瞭解過React的靚仔就要說瞭,Fiber就是一個虛擬dom樹;確實如此,但是16版本之前的React也存在虛擬dom樹,為什麼要用Fiber替代呢?

眾所周知(可能有靚仔不知道),16.8之前React還沒引入Fiber概念,Reconciler(協調器) 會在mount階段與update階段循環遞歸mountComponentupdateComponent,此時數據存儲在調用棧當中,因為是遞歸執行,所以一當開始便無法停止直到遞歸執行結束;如果此時頁面中的節點非常多我們要等到遞歸結束可能要耗費大量的時間,而且在此之間用戶會覺得卡頓,這對用戶來說絕對稱不上是好的體驗;

因此在16版本之後React有瞭異步可中斷更新雙緩存的概念,也就是我們熟知的同步並發模式Concurrent模式,那麼這些跟Fiber有什麼關系呢?

Fiber節點React源碼

首先我們來看一段關於Fiber節點的React源碼

function FiberNode(tag, pendingProps, key, mode) {
  // Instance
  //靜態屬性
  this.tag = tag;//
  this.key = key;
  this.elementType = null;//
  this.type = null;//類型
  this.stateNode = null; // Fiber
  //關聯屬性
  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當中存在很多屬性,我們大體將他們分為三類:

  • 靜態屬性:保存當前Fiber節點的 標簽,類型等;
  • 關聯屬性:用於連接其他Fiber節點形成Fiber樹;
  • 工作屬性:保存當前Fiber節點的動態工作單元;

而多個Fiber節點之間正是通過關聯屬性的連接形成一個Fiber樹;因為每一個Fiber節點都是相互獨立的,因此Fiber節點之間通過指針指向的方式產生聯系,return指向的是父級節點,child指向的是子節點,sibling指向的是兄弟節點;

如下列這段JSX代碼為例

<div className="App">
  <div className='div1'>
    <div className='div2'>
    </div>
  </div>
  <div className='div3'>
  </div>
</div>

最終該JSX產生的樹結構為

Fiber樹的每個節點都是相互獨立的,利用指針指向讓他們關聯在一起;那麼我們是不是可以說Fiber樹就是一個鏈表,關於什麼是鏈表,可以參考我這篇博文 《作為前端你是否瞭解鏈表這種數據結構?》

Fiber樹是鏈表

可能現在就有靚仔要問瞭,為什麼React要選用鏈表這種數據結構搭建Fiber架構

我是這麼考慮的

  • 節點獨立
  • 節省操作時間
  • 利於雙緩存與異步可中斷更新操作

節點獨立

不知道有沒有靚仔會說ReactFiber架構拿父節點的child存子節點拿子節點的return存父節點怎麼就節點獨立瞭呢?這位靚仔貧道建議你再去學一下一般類型和引用類型;父節的child存的是子節點的內存地址,子節點的return存的是父節點的內存地址,因此並不會占用太多空間,說白瞭他們隻是有一層關系將節點綁定在一起,但是這層關系並不是包含關系;就比如你女朋友是你女朋友,你是你一樣,你們是情侶關系,並不是占有關系(不提倡啊!自由戀愛,人格獨立);

節省操作時間與單向操作

如果Fiber樹並不是鏈表這種數據結構而是數組這種數據結構會怎麼樣呢?我們都知道數組的存儲需要在內存中開辟一長串有序的內存,如果我把中間的某個元素刪除,那麼後面的所有元素都要向上移動一個存儲空間,如果現在我有1000個節點,我把第一個節點刪瞭,那麼後面的999個節點都需要在內存空間上向上移動一位,這顯然是非常消耗時間的;但是如果是鏈表的話我們隻需要將指針解綁,移動到上一位節點或者下一節點就能形成一個新的鏈表,這在時間上來說是非常有優勢的;因為是 節點間相互獨立因此我們僅僅隻需要對指針進行操作並且它的操作是單向的我們不需要進行雙向解綁;

我們繼續以這段JSX為例

<div className="App">
  <div className='div1'>
    <div className='div2'>
    </div>
  </div>
  <div className='div3'>
  </div>
</div>

如果此時我們要將class為div1的節點刪除fiber是如何操作的?我們用圖來解釋

由圖所示,我們隻需要將App的child指針改為div2,將div2的return指針改為App即可,然後我們便可以對div1與div3進行銷毀;

利於雙緩存與異步可中斷更新操作

異步可中斷更新

我隻能說React為瞭給用戶良好的使用感受確實是下足瞭功夫,在React16之前React還采取著原始的同步更新,但是在在16之後React推出瞭concurrent模式也就是同步並發模式,在concurrent模式下你的mountupdate都將成為異步可中斷更新,至於react為什麼要推出異步可中斷更新可參考我這篇文章 《重學React之為什麼需要Scheduler》

現在我們用最直觀的瀏覽器反饋來看一下Concurrent模式Legacy模式的區別

我們看看Legacy模式下的Performance的監聽

可以看到所有的render階段方法都在同一個Task完成,如果運行時間過長將會造成卡頓;

我們再看Concurrent模式下的Performance的監聽

concurrent模式下會react的render階段會被分為若幹個時長為5ms的Task

這一切歸功於Scheduler調度器的功勞,因為16之前的React沒有Scheduler所以采用的是所以采用的是遞歸的方式將數據存儲在調用棧當中,遞歸一旦開始便無法停止,所以後來有瞭Scheduler;而采用鏈表這種數據結構(Fiber)存儲數據卻能很好的中斷遍歷;我們來看看Concurrent模式下的入口函數

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

可以看到當shouldYield() 為true時workLoopConcurrent方法將會中斷工作,而shouldYield() 對應的正是scheduler是否需要更新調度的狀態

雙緩存

雙緩存的概念在座的靚仔應該都清楚,React在運行時會有兩棵Fiber樹mount階段隻有workInProgress Fiber樹), 一顆是current Fiber樹,對應當前展示的內容,一顆是workInProgress Fiber樹對應的是正在構建的Fiber樹,在mount階段的首次創建會創建一個fiberRootNode的根節點,fiberRootNode 有一個current工作單元屬性,來回指向Fiber樹,當workInProgess Fiber樹構建完成之後current就指向workInprogress Fiber樹,此時workInProgess Fiber樹變為current Fiber樹,而current Fiber樹將變為workInProgess Fiber樹,由於這一切都是在內存中進行的,所以稱之為雙緩存;

而這一切剛好運用瞭鏈表的靈活指向,不斷形成一個新的鏈表;

以上就是React Fiber 鏈表操作原理詳解的詳細內容,更多關於React Fiber 鏈表的資料請關註WalkonNet其它相關文章!

推薦閱讀: