深刻react源碼 之深刻了解fiber與fiber架構(3)

建立fiber樹的調度算法

  在上一篇介紹fiber的屬性時,咱們簡單提到了一嘴,fiber的建立過程是一個深度優先的過程。
  在之前咱們作深度優先遍歷最多見的一種方法是,,使用樹形結構,先處理父節點,在對每一個子節點進行遞歸操做以達到深度遍歷的目的,這是最多見的方式。
  咱們來簡單模擬一下~
react

首先假設咱們有一個以下虛擬dom的結構 :git

let vdomTree = {
  id: 1,
  children: [
    { id: 2, children: [
        { id: 3, children: null },
        { id: 4, children: null }
      ]
    },
    {
      id: 5,
      children: [ { id: 6, children: null } ]
    }
  ]
}

複製代碼
function beginWork(vdomNode) {
  // 這裏對傳進來的vdom節點作一些處理
  // 好比建立實例 執行週期 作diff等等
  
  // 最後返回處理後的虛擬dom
  return vdomNode
}
複製代碼

而後咱們將這棵虛擬dom樹做爲參數傳給一個用來深度遍歷的函數中github

function deep(parentVdomTree) {
  console.log(id)
  // 對當前這個父節點自己作一些處理
  let vdomAfterTreatment = beginWork(parentVdomTree)
  // 而後獲取到當前節點的子節點
  let children = vdomAfterTreatment.children
  if (!children) return
  children.forEach((child) => {
    // 對它的每一個子節點進行一樣的操做
    // 這樣每一個子節點以及孫子,曾孫子等等節點都會作同樣的處理
    deep(child)
  })
}
deep(vdomTree)
複製代碼

  經過以上方式,咱們能夠容易的對該dom樹中的全部節點都作一些邏輯上的操做,好比生成dom實例,或class實例,執行生命週期,或作diff對比等等。
  若是把上面的tree放到瀏覽器中跑一跑的話,控制檯會依次打印出1, 2, 3, 4, 5, 6
  這也正對應了react16發佈時候官方的展現圖 :

  可是這樣有一個最大的缺點,那就是一旦咱們開始了遞歸遍歷,那麼直到該樹結構中的全部節點都被遍歷完以前,該deep函數的遞歸是不會中止的,也就是說,deep函數的調用棧會不停的入棧和出棧直到全部的節點都遍歷完成。
  這樣的話,一旦咱們的應用中存在着成千上萬的嵌套的節點的話,那這個deep的過程可能就會很是的冗長,就十分有可能形成頁面的卡頓,這也是react在16以前一直未解決的問題。
  因此若是要是想解決這個問題,咱們必需要設計一種能夠隨時暫停,退出當前deep調用棧的方法。這種方法,就是react16中新的調度算法。
  接下來咱們來看一看react16中新的調度算法長成什麼樣
在一個叫作ReactFiberScheduler.js的文件中咱們能夠找到一個叫作workLoop的方法:
算法

  能夠看到該方法中經過while循環來觸發了一個叫作performUnitOfWork的方法。這個方法中就會根據fiber的不一樣類型來作不一樣的邏輯處理以及生成下一個新的fiber。
  你們可能會注意到該方法根據傳進來的isYieldy分紅了兩個部分,這個isYieldy就表示本次的更新是否容許中斷。當沒有使用ConcurrentMode的時候或者本次更新已經超時的時候,這個isYieldy是false,也就是不容許中斷,這兩種狀況都會把剩下全部的節點從頭至尾一把梭,當節點賊雞兒多的時候會卡。
  當isYieldy是true的時候,就是容許暫停的時候,會根據shouldYieldToRenderer方法來判斷是否要中斷當前的更新。這個方法內部會根據本次更新分配到的優先級,也就是我們上一篇文章裏提到的那個expirationTime以及如今react運行的時間還有瀏覽器每一幀剩餘的時間來判斷當前的時間片是否還夠用。至於具體怎麼判斷咱回頭再聊~你們就先記住,若是使用了異步更新模式的話,每處理完一個fiber節點,都會在這裏根據時間判斷一下是否要跳出這個loop循環。這就是react16中所謂的"時間分片"。

  接下來是一個比較關鍵的函數performUnitOfWork,咱們來看一下這個函數

  能夠看到這個函數中傳進來一個叫作workInProgress的東西。這個workInProgress其實就是本次要被調度的fiber,就至關於我們上面寫的那個vdomTree,只不過我們上面傳的是假的vdom,這裏傳進來的是react中的fiber。
  這個fiber傳遞進來以後被傳進了一個叫作beginWork的函數,這個函數內部就是要對這個fiber進行一些處理,好比函數類型的fiber要被執行一下,class類型的fiber要被new一下之類的等等操做,最後返回一個next,這個next就是當前這個fiber的firstChild的fiber。咱們在上一篇介紹fiber的屬性的時候提到過,每一個節點只有一個child屬性指向它的firstChild,這裏返回的next就是這個firstChild。而後這個next會被返回到workLoop函數中,workLoop中判斷時間片是否還有剩餘,有剩餘的話用這個next做爲下一輪的workInProgress再繼續進行調度,沒有剩餘的話就直接跳出循環將線程交還給瀏覽器的ui線程。
  到了這裏應該能夠理解,每次會被使用到的fiber,都是在上一輪的beginWork中,根據它的父節點建立的。那麼您可能會問,最初一開始傳進來的那個fiber是哪兒來的,這個不着急,我們下一篇再說~
  這裏還有一個重點,咱們總說react是深度優先,但是咱們並無看到相似forEach循環之類的,而且每次咱們返回的next,都是firstChild,也就是一個單獨的fiebr節點,並無把全部的子節點都返回,那麼react究竟是怎麼深度的呢?答案在completeUnitOfWork這個函數中。
  首先在看這個函數以前,咱們回顧一下上一篇介紹fiber的屬性,每一個fiber都有個child屬性指向它的firstChild,還有一個屬性叫sibling指向它的兄弟節點。這個workLoop中,當判斷到next,也就是當前這個節點的firstChild === null的也就是當前節點再沒有子節點的時候就會進入completeUnitOfWork,而後completeUnitOfWork還會返回一個next。那這個時候問題就來了,beginWork和completeUnitOfWork返回的next你猜究竟是不是一個東西呢?
  答案: 是!!!


  那就見鬼了!!!
  確定不是啦~若是倆真是一個東西的話,beginWork返回null,說明當前節點沒有子節點,可是若是它有兄弟節點或者兄弟節點有子節點呢?
  咱們來舉個栗子。假設有以下一個jsx結構 :

  其中藍色線表明child,綠色線表明sibling,紅色線表明return。這個就是react中最後會真實生成的fiber樹的結構。以一種 (樹+鏈) 表的方式被建立出來。
  下面要說的過程略繞的,建議你們打打斷點,本身畫畫圖啥的。若是你們不想在源碼裏打斷點的話,我準備了一個簡化版的demo  點這裏
  1. 如今咱們把MyClass的fiber傳給performUnitOfWork,MyClass的fiber進入beginWork,beginWork發現MyClass下有三個子節點,因而會循環這個三個子節點,對三個子節點分別建立對應的fiber,可是!最後只返回firstChild,也就是這個h1的fiber,這個h1的fiber會做爲next,返回到workLoop,以後workLoop的while循環會用h1的fiber做爲新的workInProgress進行performUnitOfWork。
  2. h1進入performUnitOfWork後會進入beginWork,beginWork會返回next,next就是h1的firstChild。可是這個時候注意看,h1下沒有任何的東西,也就是說,h1的next是null。這個時候我們看上線的performUnitOfWork的源碼,發現若是next是null的話就會進入completeUnitOfWork這個函數中。
  咱們來看一下這個completeUnitOfWork函數:

  函數中省略了一些代碼,我們主要看react是怎麼遍歷的就好~
  首先能夠看到把當前的這個workInProgress傳遞進來也就是這個fiber,注意!傳進來的不是next,是next的爹。在我們上面這個例子中,傳進來的就是這個h1的fiber。
  3. 以後會進入一個while循環,循環先獲取到它的return以及sibling。return在上一篇文章中提到它表示當前節點的父節點,sibling表示當前節點的兄弟節點。在我們的例子是就是h1的父節點和兄弟節點,分別是MyClass和div的fiber。
  下面省略了一堆邏輯代碼以後,會走到一些判斷,這些判斷和外層這個while循環就是一個深度遍歷的過程。
  4. 首先是先判斷了sibling(div的fiber),也就是看當前節點是否有兄弟節點,若是有兄弟節點的話,直接就把兄弟節點做爲next返回到performUnitOfWork中,而後再把這個next返回給workLoop進行下一輪的循環。此時返回的是div的fiber。
  5. 接下來div的fiber進入workLoop後會從新進入performUnitOfWork。而後再進入beginWork,以後beginWork中發現div有倆子節點,一個h2一個h3,因而循環這倆節點,分別建立出h2和h3對應的fiber,以後h2做爲firstChild被返回給next,本次next不爲null,因此直接被返回到workLoop中,以後h2的fiber做爲下一輪的workInProgress進入performUnitOfWork和beginWork。
  6. 此時h2下沒有子節點,因而beginWork返回null,next是null會進入completeUnitOfWork,此時completeUnitOfWork中的參數是h2的fiber。
  7. 一樣,找到h2的fiber的兄弟節點和父節點,例子中分別是h3的fiber的div的fiber。而後先判斷是否有兄弟節點,發現有是h3的fiber,把h3的fiber做爲next返回。以後h3的fiber會走同樣的邏輯從新進入performUnitOfWork和beginWork
  8. beginWork返回h3的next,發現h3下沒有child,是個null,因而進入completeUnitOfWork。
  9. 一樣找到h3的兄弟節點和父節點。發現h3的兄弟節點是個null,父節點是div的fiber。往下走到判斷,第一步看兄弟節點,發現是null,往下走,發現returnFiber不爲null,是div,因而讓當前的這個workInProgress變成div的fiber,continue這個while循環。
  10. 至關於從新進入到completeUnitOfWork這個函數,可是此時的workInProgress已經變成了div的fiber了。一樣的,先找到div的兄弟節點和父節點,本例中分別是span和MyClass。
  11. 往下走,發現有兄弟節點,因而讓兄弟節點做爲next被返回,此時的next是span的fiber。
  12. span的fiber從新進入perfo和beginWork,beginWork返回next,可是span沒有子節點是個null,因而span的fiber進入completeUnitOfWork。
  13. completeUnitOfWork中獲取到span 的兄弟節點和父節點,分別是 null 和 MyClass。以後往下走到判斷,發現span沒有兄弟節點,因而用父節點也就是MyClass做爲workInProgress,continue這個while。
  14. 此時的workInProgress是MyClass的fiber,找到他的父節點和兄弟節點,發現他的父節點和兄弟節點都是null(其實MyClass的父節點應該是一個叫RootFiber的東西,這個RootFiber我們下一篇說,這裏先暫時理解成null),因而最終這個completeUnitOfWork函數返回了 null 做爲next。
  15. 到此爲止,終於,next已經決計爲null了,因而null被返回到workLoop中,workLoop發現nextUnitOfWork是null,因而跳出loop循環,react結束建立fiber的過程。

  我們經過一個例子講解了react建立fiber樹的過程,這個過程總結一下就是從根兒開始往下遍歷,一旦某個節點的child是null了,就去遍歷這個節點的兄弟節點,一旦這個節點的兄弟節點也是null了,就去遍歷他父節點的兄弟節點。而後要處理下一個fiber以前,給了react一個機會,一個能夠中斷渲染fiber,能夠將線程交還給瀏覽器的機會。此時被中斷的fiber會記錄在那個nextUnitOfWork全局變量上,這樣當下一次再回來繼續的時候能夠很輕鬆的找到上一次被打斷的地方。
數組

  react中不少的地方都用到了這種相似的算法,能夠說這個算法和fiber數據結構是徹底相輔相成的。這就是react中最主要的fiber架構。
瀏覽器

P.S. 以前有同事問我,若是使用廣度遍歷的話,應該也能夠作到隨時暫停遍歷的過程,那爲啥react不用廣度遍歷而非得用深度遍歷。
  其實關於這個問題,主要是由於若是咱們的dom節點特別的多的話,那麼咱們就不得不維護一個特別大的數組棧了,而後每次當中斷了遍歷後,下一次再想遍歷的話,還須要再去這個數組中去找上一次的節點。可是咱們若是使用fiber這種數據結構的話,fiber是一種自帶上下文的對象結構,因此能夠很輕鬆的配合深度遍歷找到上一次中斷的地方。這個是我認爲react不採用廣度遍歷而採用深度遍歷的緣由。數據結構

gayhub地址:github.com/y805939188/… 跪求大佬賞星星架構

下一篇: react時間分片(time split)
dom

相關文章
相關標籤/搜索