作者 | F(x) Team - 冷卉
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SZzYmN4gjMmdDMzQWM0IWOhNjY0YDMhZTY5MGOjljYy8CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
Fiber 設計思想
Fiber 是對 React 核心算法的重構,facebook 團隊使用兩年多的時間去重構 React 的核心算法,在React16 以上的版本中引入了 Fiber 架構,其中的設計思想是非常值得我們學習的。
為什麼需要 Fiber
我們知道,在浏覽器中,頁面是一幀一幀繪制出來的,渲染的幀率與裝置的重新整理率保持一緻。一般情況下,裝置的螢幕重新整理率為1s 60次,當每秒内繪制的幀數(FPS)超過60時,頁面渲染是流暢的;而當FPS小于60時,會出現一定程度的卡頓現象。下面來看完整的一幀中,具體做了哪些事情:
- 首先需要處理輸入事件,能夠讓使用者得到最早的回報
- 接下來是處理定時器,需要檢查定時器是否到時間,并執行對應的回調
- 接下來處理 Begin Frame(開始幀),即每一幀的事件,包括 window.resize、scroll、media query change 等
- 接下來執行請求動畫幀 requestAnimationFrame(rAF),即在每次繪制之前,會執行 rAF 回調
- 緊接着進行 Layout 操作,包括計算布局和更新布局,即這個元素的樣式是怎樣的,它應該在頁面如何展示
- 接着進行 Paint 操作,得到樹中每個節點的尺寸與位置等資訊,浏覽器針對每個元素進行内容填充
- 到這時以上的六個階段都已經完成了,接下來處于空閑階段(Idle Peroid),可以在這時執行 requestIdleCallback 裡注冊的任務(後面會詳細講到這個 requestIdleCallback ,它是 React Fiber 實作的基礎)
js引擎和頁面渲染引擎是在同一個渲染線程之内,兩者是互斥關系。如果在某個階段執行任務特别長,例如在定時器階段或Begin Frame階段執行時間非常長,時間已經明顯超過了16ms,那麼就會阻塞頁面的渲染,進而出現卡頓現象。
在 react16 引入 Fiber 架構之前,react 會采用遞歸對比虛拟DOM樹,找出需要變動的節點,然後同步更新它們,這個過程 react 稱為reconcilation(協調)。在reconcilation期間,react 會一直占用浏覽器資源,會導緻使用者觸發的事件得不到響應。實作的原理如下所示:
這裡有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 的,為什麼呢?原因是二者的優化思路不一樣:
- Vue 是基于 template 和 watcher 的元件級更新,把每個更新任務分割得足夠小,不需要使用到 Fiber 架構,将任務進行更細粒度的拆分
- React 是不管在哪裡調用 setState,都是從根節點開始更新的,更新任務還是很大,需要使用到 Fiber 将大任務分割為多個小任務,可以中斷和恢複,不阻塞主程序執行高優先級的任務
下面,讓我們走進 Fiber 的世界,看看具體是怎麼實作的。
什麼是 Fiber
Fiber 可以了解為是一個執行單元,也可以了解為是一種資料結構。
一個執行單元
Fiber 可以了解為一個執行單元,每次執行完一個執行單元,react 就會檢查現在還剩多少時間,如果沒有時間則将控制權讓出去。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。
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左右。
requestIdleCallback
requestIdleCallback 也是 react Fiber 實作的基礎 api 。我們希望能夠快速響應使用者,讓使用者覺得夠快,不能阻塞使用者的互動,
requestIdleCallback
能使開發者在主事件循環上執行背景和低優先級的工作,而不影響延遲關鍵事件,如動畫和輸入響應。正常幀任務完成後沒超過16ms,說明有多餘的空閑時間,此時就會執行requestIdleCallback裡注冊的任務。
具體的執行流程如下,開發者采用
requestIdleCallback
方法注冊對應的任務,告訴浏覽器我的這個任務優先級不高,如果每一幀記憶體在空閑時間,就可以執行注冊的這個任務。另外,開發者是可以傳入
timeout
參數去定義逾時時間的,如果到了逾時時間了,浏覽器必須立即執行,使用方法如下:
window.requestIdleCallback(callback, { timeout: 1000 })
。浏覽器執行完這個方法後,如果沒有剩餘時間了,或者已經沒有下一個可執行的任務了,React應該歸還控制權,并同樣使用
requestIdleCallback
去申請下一個時間片。具體的流程如下圖:
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
中定義的三個任務均執行完畢。列印結果如下:
多幀執行
在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,隻能執行一個任務,同理,在第二幀、第三幀的時間也隻夠執行一個任務。所有這三個任務分别是在三幀中分别完成的。列印結果如下:
浏覽器一幀的時間并不嚴格是16ms,是可以動态控制的(如第三幀剩餘時間為49.95ms)。如果子任務的時間超過了一幀的剩餘時間,則會一直卡在這裡執行,直到子任務執行完畢。如果代碼存在死循環,則浏覽器會卡死。如果此幀的剩餘時間大于0(有空閑時間)或者已經逾時(上文定義了 timeout 時間為1000,必須強制執行了),且當時存在任務,則直接執行該任務。如果沒有剩餘時間,則應該放棄執行任務控制權,把執行權交還給浏覽器。如果多個任務執行總時間小于空閑時間的話,是可以在一幀内執行多個任務的。
Fiber連結清單結構設計
Fiber結構是使用連結清單實作的,
Fiber tree
實際上是個單連結清單樹結構,詳見ReactFiber.js源碼,在這裡我們看看Fiber的連結清單結構是怎樣的,了解了這個連結清單結構後,能更快地了解後續 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(後面我們稱之為連結清單)。如下圖所示:
其他的屬性還有
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
時采用的是後序周遊方法:
- 從頂點開始周遊
- 如果有大兒子,先周遊大兒子;如果沒有大兒子,則表示周遊完成
-
大兒子:
a. 如果有弟弟,則傳回弟弟,跳到2
b. 如果沒有弟弟,則傳回父節點,并标志完成父節點周遊,跳到2
d. 如果沒有父節點則标志周遊結束
定義樹結構:
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
的具體步驟為:
- 如果目前節點需要更新,則打
更新目前節點狀态(props, state, context等)tag
- 為每個子節點建立fiber。如果沒有産生
,則結束該節點,把child fiber
歸并到effect list
,把此節點的return
節點作為下一個周遊節點;否則把sibling
節點作為下一個周遊節點child
- 如果有剩餘時間,則開始下一個節點,否則等下一次主線程空閑再開始下一個節點
- 如果沒有下一個節點了,進入
狀态,此時pendingCommit
收集完畢,結束。effect list
effect list
的周遊順序如下所示:
周遊子虛拟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 源碼繼續研究。