作者 | F(x) Team - 冷卉
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
的具體步驟為:
- 如果當前節點需要更新,則打
tag
更新當前節點狀態(props, state, context等) - 為每個子節點創建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 源碼繼續研究。