天天看點

走進 React Fiber 的世界

作者 | F(x) Team - 冷卉
走進 React Fiber 的世界

Fiber 設計思想

Fiber 是對 React 核心算法的重構,facebook 團隊使用兩年多的時間去重構 React 的核心算法,在React16 以上的版本中引入了 Fiber 架構,其中的設計思想是非常值得我們學習的。

為什麼需要 Fiber

我們知道,在浏覽器中,頁面是一幀一幀繪制出來的,渲染的幀率與裝置的重新整理率保持一緻。一般情況下,裝置的螢幕重新整理率為1s 60次,當每秒内繪制的幀數(FPS)超過60時,頁面渲染是流暢的;而當FPS小于60時,會出現一定程度的卡頓現象。下面來看完整的一幀中,具體做了哪些事情:

走進 React Fiber 的世界
  1. 首先需要處理輸入事件,能夠讓使用者得到最早的回報
  2. 接下來是處理定時器,需要檢查定時器是否到時間,并執行對應的回調
  3. 接下來處理 Begin Frame(開始幀),即每一幀的事件,包括 window.resize、scroll、media query change 等
  4. 接下來執行請求動畫幀 requestAnimationFrame(rAF),即在每次繪制之前,會執行 rAF 回調
  5. 緊接着進行 Layout 操作,包括計算布局和更新布局,即這個元素的樣式是怎樣的,它應該在頁面如何展示
  6. 接着進行 Paint 操作,得到樹中每個節點的尺寸與位置等資訊,浏覽器針對每個元素進行内容填充
  7. 到這時以上的六個階段都已經完成了,接下來處于空閑階段(Idle Peroid),可以在這時執行 requestIdleCallback 裡注冊的任務(後面會詳細講到這個 requestIdleCallback ,它是 React Fiber 實作的基礎)

js引擎和頁面渲染引擎是在同一個渲染線程之内,兩者是互斥關系。如果在某個階段執行任務特别長,例如在定時器階段或Begin Frame階段執行時間非常長,時間已經明顯超過了16ms,那麼就會阻塞頁面的渲染,進而出現卡頓現象。

在 react16 引入 Fiber 架構之前,react 會采用遞歸對比虛拟DOM樹,找出需要變動的節點,然後同步更新它們,這個過程 react 稱為reconcilation(協調)。在reconcilation期間,react 會一直占用浏覽器資源,會導緻使用者觸發的事件得不到響應。實作的原理如下所示:

走進 React Fiber 的世界

這裡有7個節點,B1、B2 是 A1 的子節點,C1、C2 是 B1 的子節點,C3、C4 是 B2 的子節點。傳統的做法就是采用深度優先周遊去周遊節點,具體代碼如下:

const root = {
  key: 'A1',
  children: [{
    key: 'B1',
    children: [{
      key: 'C1',
      children: []
    }, {
      key: 'C2',
      children: []
    }]
  }, {
    key: 'B2',
    children: [{
      key: 'C3',
      children: []
    }, {
      key: 'C4',
      children: []
    }]
  }]
}
const walk = dom => {
  console.log(dom.key)
  dom.children.forEach(child => walk(child))
}
walk(root)           

列印:

A1
B1
C1
C2
B2
C3
C4           

這種周遊是遞歸調用,執行棧會越來越深,而且不能中斷,中斷後就不能恢複了。遞歸如果非常深,就會十分卡頓。如果遞歸花了100ms,則這100ms浏覽器是無法響應的,代碼執行時間越長卡頓越明顯。傳統的方法存在不能中斷和執行棧太深的問題。

是以,為了解決以上的痛點問題,React希望能夠徹底解決主線程長時間占用問題,于是引入了 Fiber 來改變這種不可控的現狀,把渲染/更新過程拆分為一個個小塊的任務,通過合理的排程機制來調控時間,指定任務執行的時機,進而降低頁面卡頓的機率,提升頁面互動體驗。通過Fiber架構,讓reconcilation過程變得可被中斷。适時地讓出CPU執行權,可以讓浏覽器及時地響應使用者的互動。

React16中使用了 Fiber,但是 Vue 是沒有 Fiber 的,為什麼呢?原因是二者的優化思路不一樣:

  1. Vue 是基于 template 和 watcher 的元件級更新,把每個更新任務分割得足夠小,不需要使用到 Fiber 架構,将任務進行更細粒度的拆分
  2. React 是不管在哪裡調用 setState,都是從根節點開始更新的,更新任務還是很大,需要使用到 Fiber 将大任務分割為多個小任務,可以中斷和恢複,不阻塞主程序執行高優先級的任務

下面,讓我們走進 Fiber 的世界,看看具體是怎麼實作的。

什麼是 Fiber

Fiber 可以了解為是一個執行單元,也可以了解為是一種資料結構。

一個執行單元

Fiber 可以了解為一個執行單元,每次執行完一個執行單元,react 就會檢查現在還剩多少時間,如果沒有時間則将控制權讓出去。React Fiber 與浏覽器的核心互動流程如下:

走進 React Fiber 的世界

首先 React 向浏覽器請求排程,浏覽器在一幀中如果還有空閑時間,會去判斷是否存在待執行任務,不存在就直接将控制權交給浏覽器,如果存在就會執行對應的任務,執行完成後會判斷是否還有時間,有時間且有待執行任務則會繼續執行下一個任務,否則就會将控制權交給浏覽器。這裡會有點繞,可以結合上述的圖進行了解。

Fiber 可以被了解為劃分一個個更小的執行單元,它是把一個大任務拆分為了很多個小塊任務,一個小塊任務的執行必須是一次完成的,不能出現暫停,但是一個小塊任務執行完後可以移交控制權給浏覽器去響應使用者,進而不用像之前一樣要等那個大任務一直執行完成再去響應使用者。

一種資料結構

Fiber 還可以了解為是一種資料結構,React Fiber 就是采用連結清單實作的。每個 Virtual DOM 都可以表示為一個 fiber,如下圖所示,每個節點都是一個 fiber。一個 fiber包括了 child(第一個子節點)、sibling(兄弟節點)、return(父節點)等屬性,React Fiber 機制的實作,就是依賴于以下的資料結構。在下文中會講到基于這個連結清單結構,Fiber 究竟是如何實作的。

PS:這裡需要說明一下,Fiber 是 React 進行重構的核心算法,fiber 是指資料結構中的每一個節點,如下圖所示的A1、B1都是一個 fiber。

走進 React Fiber 的世界

requestAnimationFrame

在 Fiber 中使用到了requestAnimationFrame,它是浏覽器提供的繪制動畫的 api 。它要求浏覽器在下次重繪之前(即下一幀)調用指定的回調函數更新動畫。

例如我想讓浏覽器在每一幀中,将頁面 div 元素的寬變長1px,直到寬度達到100px停止,這時就可以采用

requestAnimationFrame

來實作這個功能。

<body>
  <div id="div" class="progress-bar "></div>
  <button id="start">開始動畫</button>
</body>

<script>
  let btn = document.getElementById('start')
  let div = document.getElementById('div')
  let start = 0
  let allInterval = []

  const progress = () => {
    div.style.width = div.offsetWidth + 1 + 'px'
    div.innerHTML = (div.offsetWidth) + '%'
    if (div.offsetWidth < 100) {
      let current = Date.now()
      allInterval.push(current - start)
      start = current
      requestAnimationFrame(progress)
    } else {
      console.log(allInterval) // 列印requestAnimationFrame的全部時間間隔
    }
  }

  btn.addEventListener('click', () => {
    div.style.width = 0
    let currrent = Date.now()
    start = currrent
    requestAnimationFrame(progress)
    console.log(allInterval)
  })
</script>           

浏覽器會在每一幀中,将div的寬度變寬1px,知道到達100px為止。列印出每一幀的時間間隔如下,大約是16ms左右。

走進 React Fiber 的世界

requestIdleCallback

requestIdleCallback 也是 react Fiber 實作的基礎 api 。我們希望能夠快速響應使用者,讓使用者覺得夠快,不能阻塞使用者的互動,

requestIdleCallback

能使開發者在主事件循環上執行背景和低優先級的工作,而不影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成後沒超過16ms,說明有多餘的空閑時間,此時就會執行requestIdleCallback裡注冊的任務。

具體的執行流程如下,開發者采用

requestIdleCallback

方法注冊對應的任務,告訴浏覽器我的這個任務優先級不高,如果每一幀記憶體在空閑時間,就可以執行注冊的這個任務。另外,開發者是可以傳入

timeout

參數去定義逾時時間的,如果到了逾時時間了,浏覽器必須立即執行,使用方法如下:

window.requestIdleCallback(callback, { timeout: 1000 })

。浏覽器執行完這個方法後,如果沒有剩餘時間了,或者已經沒有下一個可執行的任務了,React應該歸還控制權,并同樣使用

requestIdleCallback

去申請下一個時間片。具體的流程如下圖:

走進 React Fiber 的世界

window.requestIdleCallback(callback)

callback

中會接收到預設參數

deadline

,其中包含了以下兩個屬性:

  • timeRamining 傳回目前幀還剩多少時間供使用者使用
  • didTimeout 傳回 callback 任務是否逾時

requestIdleCallback

方法非常重要,下面分别講兩個例子來了解這個方法,在每個例子中都需要執行多個任務,但是任務的執行時間是不一樣的,下面來看浏覽器是如何配置設定時間執行這些任務的:

一幀執行

直接執行task1、task2、task3,各任務的時間均小于16ms:

let taskQueue = [
  () => {
    console.log('task1 start')
    console.log('task1 end')
  },
  () => {
    console.log('task2 start')
    console.log('task2 end')
  },
  () => {
    console.log('task3 start')
    console.log('task3 end')
  }
]

const performUnitWork = () => {
  // 取出第一個隊列中的第一個任務并執行
  taskQueue.shift()()
}

const workloop = (deadline) => {
  console.log(`此幀的剩餘時間為: ${deadline.timeRemaining()}`)
  // 如果此幀剩餘時間大于0或者已經到了定義的逾時時間(上文定義了timeout時間為1000,到達時間時必須強制執行),且當時存在任務,則直接執行這個任務
  // 如果沒有剩餘時間,則應該放棄執行任務控制權,把執行權交還給浏覽器
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
    performUnitWork()
  }

  // 如果還有未完成的任務,繼續調用requestIdleCallback申請下一個時間片
  if (taskQueue.length > 0) {
    window.requestIdleCallback(workloop, { timeout: 1000 })
  }
}

requestIdleCallback(workloop, { timeout: 1000 })           

上面定義了一個任務隊列

taskQueue

,并定義了

workloop

函數,其中采用

window.requestIdleCallback(workloop, { timeout: 1000 })

去執行taskQueue中的任務。每個任務中僅僅做了

console.log

的工作,時間是非常短的,浏覽器計算此幀中還剩餘15.52ms,足以一次執行完這三個任務,是以在此幀的空閑時間中,

taskQueue

中定義的三個任務均執行完畢。列印結果如下:

走進 React Fiber 的世界

多幀執行

在task1、task2、task3中加入睡眠時間,各自執行時間超過16ms:

const sleep = delay => {
  for (let start = Date.now(); Date.now() - start <= delay;) {}
}

let taskQueue = [
  () => {
    console.log('task1 start')
    sleep(20) // 已經超過一幀的時間(16.6ms),需要把控制權交給浏覽器
    console.log('task1 end')
  },
  () => {
    console.log('task2 start')
    sleep(20) // 已經超過一幀的時間(16.6ms),需要把控制權交給浏覽器
    console.log('task2 end')
  },
  () => {
    console.log('task3 start')
    sleep(20) // 已經超過一幀的時間(16.6ms),需要把控制權交給浏覽器
    console.log('task3 end')
  }
]           

基于以上的例子做了部分改造,讓taskQueue中的每個任務的執行時間都超過16.6ms,看列印結果知道浏覽器第一幀的空閑時間為14ms,隻能執行一個任務,同理,在第二幀、第三幀的時間也隻夠執行一個任務。所有這三個任務分别是在三幀中分别完成的。列印結果如下:

走進 React Fiber 的世界

浏覽器一幀的時間并不嚴格是16ms,是可以動态控制的(如第三幀剩餘時間為49.95ms)。如果子任務的時間超過了一幀的剩餘時間,則會一直卡在這裡執行,直到子任務執行完畢。如果代碼存在死循環,則浏覽器會卡死。如果此幀的剩餘時間大于0(有空閑時間)或者已經逾時(上文定義了 timeout 時間為1000,必須強制執行了),且當時存在任務,則直接執行該任務。如果沒有剩餘時間,則應該放棄執行任務控制權,把執行權交還給浏覽器。如果多個任務執行總時間小于空閑時間的話,是可以在一幀内執行多個任務的。

Fiber連結清單結構設計

Fiber結構是使用連結清單實作的,

Fiber tree

實際上是個單連結清單樹結構,詳見ReactFiber.js源碼,在這裡我們看看Fiber的連結清單結構是怎樣的,了解了這個連結清單結構後,能更快地了解後續 Fiber 的周遊過程。

走進 React Fiber 的世界

以上每一個單元包含了

payload

(資料)和

nextUpdate

(指向下一個單元的指針),定義結構如下:

class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload // payload 資料
    this.nextUpdate = nextUpdate // 指向下一個節點的指針
  }
}           

接下來定義一個隊列,把每個單元串聯起來,其中定義了兩個指針:頭指針

firstUpdate

和尾指針

lastUpdate

,作用是指向第一個單元和最後一個單元,并加入了

baseState

屬性存儲React中的state狀态。如下所示:

class UpdateQueue {
  constructor() {
    this.baseState = null // state
    this.firstUpdate = null // 第一個更新
    this.lastUpdate = null // 最後一個更新
  }
}           

接下來定義兩個方法:插入節點單元(enqueueUpdate)、更新隊列(forceUpdate)。插入節點單元時需要考慮是否已經存在節點,如果不存在直接将

firstUpdate

lastUpdate

指向此節點即可。更新隊列是周遊這個連結清單,根據

payload

中的内容去更新

state

的值。

class UpdateQueue {
  //.....
  
  enqueueUpdate(update) {
    // 目前連結清單是空連結清單
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      // 目前連結清單不為空
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  
  // 擷取state,然後周遊這個連結清單,進行更新
  forceUpdate() {
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while (currentUpdate) {
      // 判斷是函數還是對象,是函數則需要執行,是對象則直接傳回
      let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = { ...currentState, ...nextState }
      currentUpdate = currentUpdate.nextUpdate
    }
    // 更新完成後清空連結清單
    this.firstUpdate = this.lastUpdate = null
    this.baseState = currentState
    return currentState
  }
}           

最後寫一個demo,執行個體化一個隊列,向其中加入很多節點,再更新這個隊列:

let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState);           

列印結果如下:

{ name:'www',age:12 }           

Fiber 節點設計

Fiber 的拆分機關是 fiber(

fiber tree

上的一個節點),實際上就是按虛拟DOM節點拆,我們需要根據虛拟dom去生成 Fiber 樹。下文中我們把每一個節點叫做 fiber 。fiber 節點結構如下,源碼詳見ReactInternalTypes.js。

{
    
    type: any, // 對于類元件,它指向構造函數;對于DOM元素,它指定HTML tag
    key: null | string, // 唯一辨別符
    stateNode: any, // 儲存對元件的類執行個體,DOM節點或與fiber節點關聯的其他React元素類型的引用
    child: Fiber | null, // 大兒子
    sibling: Fiber | null, // 下一個兄弟
    return: Fiber | null, // 父節點
    tag: WorkTag, // 定義fiber操作的類型, 詳見https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, // 指向下一個節點的指針
    updateQueue: mixed, // 用于狀态更新,回調函數,DOM更新的隊列
    memoizedState: any, // 用于建立輸出的fiber狀态
    pendingProps: any, // 已從React元素中的新資料更新,并且需要應用于子元件或DOM元素的props
    memoizedProps: any, // 在前一次渲染期間用于建立輸出的props
    // ……     
}           

fiber 節點包括了以下的屬性:

(1)type & key

  • fiber 的 type 和 key 與 React 元素的作用相同。fiber 的 type 描述了它對應的元件,對于複合元件,type 是函數或類元件本身。對于原生标簽(div,span等),type 是一個字元串。随着 type 的不同,在 reconciliation 期間使用 key 來确定 fiber 是否可以重新使用。

(2)stateNode

  • stateNode 儲存對元件的類執行個體,DOM節點或與 fiber 節點關聯的其他 React 元素類型的引用。一般來說,可以認為這個屬性用于儲存與 fiber 相關的本地狀态。

(3)child & sibling & return

  • child 屬性指向此節點的第一個子節點(大兒子)。
  • sibling 屬性指向此節點的下一個兄弟節點(大兒子指向二兒子、二兒子指向三兒子)。
  • return 屬性指向此節點的父節點,即目前節點處理完畢後,應該向誰送出自己的成果。如果 fiber 具有多個子 fiber,則每個子 fiber 的 return fiber 是 parent 。

所有 fiber 節點都通過以下屬性:child,sibling 和 return來構成一個 fiber node 的 linked list(後面我們稱之為連結清單)。如下圖所示:

走進 React Fiber 的世界

其他的屬性還有

memoizedState

(建立輸出的 fiber 的狀态)、

pendingProps

(将要改變的 props )、

memoizedProps

(上次渲染建立輸出的 props )、

pendingWorkPriority

(定義 fiber 工作優先級)等等,在這裡就不展開描述了。

Fiber 執行原理

從根節點開始渲染和排程的過程可以分為兩個階段:render 階段、commit 階段。

  • render 階段:這個階段是可中斷的,會找出所有節點的變更
  • commit 階段:這個階段是不可中斷的,會執行所有的變更

render 階段

此階段會找出所有節點的變更,如節點新增、删除、屬性變更等,這些變更 react 統稱為副作用(effect),此階段會建構一棵

Fiber tree

,以虛拟dom節點為次元對任務進行拆分,即一個虛拟dom節點對應一個任務,最後産出的結果是

effect list

,從中可以知道哪些節點更新、哪些節點增加、哪些節點删除了。

周遊流程

React Fiber

首先是将虛拟DOM樹轉化為

Fiber tree

,是以每個節點都有

child

sibling

return

屬性,周遊

Fiber tree

時采用的是後序周遊方法:

  1. 從頂點開始周遊
  2. 如果有大兒子,先周遊大兒子;如果沒有大兒子,則表示周遊完成
  3. 大兒子:

    a. 如果有弟弟,則傳回弟弟,跳到2

    b. 如果沒有弟弟,則傳回父節點,并标志完成父節點周遊,跳到2

    d. 如果沒有父節點則标志周遊結束

走進 React Fiber 的世界

定義樹結構:

const A1 = { type: 'div', key: 'A1' }
const B1 = { type: 'div', key: 'B1', return: A1 }
const B2 = { type: 'div', key: 'B2', return: A1 }
const C1 = { type: 'div', key: 'C1', return: B1 }
const C2 = { type: 'div', key: 'C2', return: B1 }
const C3 = { type: 'div', key: 'C3', return: B2 }
const C4 = { type: 'div', key: 'C4', return: B2 }

A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4

module.exports = A1           

寫周遊方法:

let rootFiber = require('./element')

const beginWork = (Fiber) => {
  console.log(`${Fiber.key} start`)
}

const completeUnitWork = (Fiber) => {
  console.log(`${Fiber.key} end`)
}

// 周遊函數
const performUnitOfWork = (Fiber) => {
  beginWork(Fiber)
  if (Fiber.child) {
    return Fiber.child
  }
  while (Fiber) {
    completeUnitWork(Fiber)
    if (Fiber.sibling) {
      return Fiber.sibling
    }
    Fiber = Fiber.return
  }
}

const workloop = (nextUnitOfWork) => {
  // 如果有待執行的執行單元則執行,傳回下一個執行單元
  while (nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }
  if (!nextUnitOfWork) {
    console.log('reconciliation階段結束')
  }
}

workloop(rootFiber)           

列印結果:

A1 start
B1 start
C1 start
C1 end // C1完成
C2 start
C2 end // C2完成
B1 end // B1完成
B2 start
C3 start
C3 end // C3完成
C4 start
C4 end // C4完成
B2 end // B2完成
A1 end // A1完成
reconciliation階段結束           

收集effect list

知道了周遊方法之後,接下來需要做的工作就是在周遊過程中,收集所有節點的變更産出

effect list

,注意其中隻包含了需要變更的節點。通過每個節點更新結束時向上歸并

effect list

來收集任務結果,最後根節點的

effect list

裡就記錄了包括了所有需要變更的結果。

收集

effect list

的具體步驟為:

  1. 如果目前節點需要更新,則打

    tag

    更新目前節點狀态(props, state, context等)
  2. 為每個子節點建立fiber。如果沒有産生

    child fiber

    ,則結束該節點,把

    effect list

    歸并到

    return

    ,把此節點的

    sibling

    節點作為下一個周遊節點;否則把

    child

    節點作為下一個周遊節點
  3. 如果有剩餘時間,則開始下一個節點,否則等下一次主線程空閑再開始下一個節點
  4. 如果沒有下一個節點了,進入

    pendingCommit

    狀态,此時

    effect list

    收集完畢,結束。

effect list

的周遊順序如下所示:

走進 React Fiber 的世界

周遊子虛拟DOM元素數組,為每個虛拟DOM元素建立子fiber:

const reconcileChildren = (currentFiber, newChildren) => {
  let newChildIndex = 0
  let prevSibling // 上一個子fiber

  // 周遊子虛拟DOM元素數組,為每個虛拟DOM元素建立子fiber
  while (newChildIndex < newChildren.length) {
    let newChild = newChildren[newChildIndex]
    let tag
    // 打tag,定義 fiber類型
    if (newChild.type === ELEMENT_TEXT) { // 這是文本節點
      tag = TAG_TEXT
    } else if (typeof newChild.type === 'string') {  // 如果type是字元串,則是原生DOM節點
      tag = TAG_HOST
    }
    let newFiber = {
      tag,
      type: newChild.type,
      props: newChild.props,
      stateNode: null, // 還未建立DOM元素
      return: currentFiber, // 父親fiber
      effectTag: INSERT, // 副作用辨別,包括新增、删除、更新
      nextEffect: null, // 指向下一個fiber,effect list通過nextEffect指針進行連接配接
    }
    if (newFiber) {
      if (newChildIndex === 0) {
        currentFiber.child = newFiber // child為大兒子
      } else {
        prevSibling.sibling = newFiber // 讓大兒子的sibling指向二兒子
      }
      prevSibling = newFiber
    }
    newChildIndex++
  }
}           

定義一個方法收集此 fiber 節點下所有的副作用,并組成

effect list

。注意每個 fiber 有兩個屬性:

  • firstEffect:指向第一個有副作用的子fiber
  • lastEffect:指向最後一個有副作用的子fiber

中間的使用

nextEffect

做成一個單連結清單。

// 在完成的時候要收集有副作用的fiber,組成effect list
const completeUnitOfWork = (currentFiber) => {
  // 後續周遊,兒子們完成之後,自己才能完成。最後會得到以上圖中的鍊條結構。
  let returnFiber = currentFiber.return
  if (returnFiber) {
    // 如果父親fiber的firstEffect沒有值,則将其指向目前fiber的firstEffect
    if (!returnFiber.firstEffect) {
      returnFiber.firstEffect = currentFiber.firstEffect
    }
    // 如果目前fiber的lastEffect有值
    if (currentFiber.lastEffect) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
      }
      returnFiber.lastEffect = currentFiber.lastEffect
    }
    const effectTag = currentFiber.effectTag
    if (effectTag) { // 說明有副作用
      // 每個fiber有兩個屬性:
      // 1)firstEffect:指向第一個有副作用的子fiber
      // 2)lastEffect:指向最後一個有副作用的子fiber
      // 中間的使用nextEffect做成一個單連結清單
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber
      } else {
        returnFiber.firstEffect = currentFiber
      }
      returnFiber.lastEffect = currentFiber
    }
  }
}           

接下來定義一個遞歸函數,從根節點出發,把全部的 fiber 節點周遊一遍,産出最終全部的

effect list

// 把該節點和子節點任務都執行完
const performUnitOfWork = (currentFiber) => {
  beginWork(currentFiber)
  if (currentFiber.child) {
    return currentFiber.child
  }
  while (currentFiber) {
    completeUnitOfWork(currentFiber) // 讓自己完成
    if (currentFiber.sibling) { // 有弟弟則傳回弟弟
      return currentFiber.sibling
    }
    currentFiber = currentFiber.return // 沒有弟弟,則找到父親,讓父親完成,父親會去找他的弟弟即叔叔
  }
}           

commit階段

commit 階段需要将上階段計算出來的需要處理的副作用一次性執行,此階段不能暫停,否則會出現UI更新不連續的現象。此階段需要根據

effect list

,将所有更新都 commit 到DOM樹上。

根據一個 fiber 的 effect list 更新視圖

根據一個 fiber 的

effect list

清單去更新視圖(這裡隻列舉了新增節點、删除節點、更新節點的三種操作):

const commitWork = currentFiber => {
  if (!currentFiber) return
  let returnFiber = currentFiber.return
  let returnDOM = returnFiber.stateNode // 父節點元素
  if (currentFiber.effectTag === INSERT) {  // 如果目前fiber的effectTag辨別位INSERT,則代表其是需要插入的節點
    returnDOM.appendChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === DELETE) {  // 如果目前fiber的effectTag辨別位DELETE,則代表其是需要删除的節點
    returnDOM.removeChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === UPDATE) {  // 如果目前fiber的effectTag辨別位UPDATE,則代表其是需要更新的節點
    if (currentFiber.type === ELEMENT_TEXT) {
      if (currentFiber.alternate.props.text !== currentFiber.props.text) {
        currentFiber.stateNode.textContent = currentFiber.props.text
      }
    }
  }
  currentFiber.effectTag = null
}           

根據全部 fiber 的 effect list 更新視圖

寫一個遞歸函數,從根節點出發,根據

effect list

完成全部更新:

const commitRoot = () => {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {
    commitWork(currentFiber)
    currentFiber = currentFiber.nextEffect
  }
  currentRoot = workInProgressRoot // 把目前渲染成功的根fiber賦給currentRoot
  workInProgressRoot = null
}           

完成視圖更新

接下來定義循環執行工作,當計算完成每個 fiber 的

effect list

後,調用 commitRoot 完成視圖更新:

const workloop = (deadline) => {
  let shouldYield = false // 是否需要讓出控制權
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1 // 如果執行完任務後,剩餘時間小于1ms,則需要讓出控制權給浏覽器
  }
  if (!nextUnitOfWork && workInProgressRoot) {
    console.log('render階段結束')
    commitRoot() // 沒有下一個任務了,根據effect list結果批量更新視圖
  }
  // 請求浏覽器進行再次排程
  requestIdleCallback(workloop, { timeout: 1000 })
}           

到這時,已經根據收集到的變更資訊,完成了視圖的重新整理操作。

總結

本文是為了讓大家對 React Fiber 能有一個大緻的了解,本文介紹了為什麼在 React 中要引入 Fiber 機制,它的設計思想是什麼,以及在代碼中是如何一點點實作的。但是仍然有很多的點沒有覆寫到,例如如何定義排程任務優先級、如何進行任務中斷與斷點恢複……感興趣的朋友可以結合 react 源碼繼續研究。

走進 React Fiber 的世界

繼續閱讀