在 react 進入你們視野之初,Virtual DOM(VDOM)的概念讓人眼前一亮,在操做真正的 DOM 以前,先經過 VDOM 先後對比得出須要更新的部分,再去操做真實的 DOM,減小了瀏覽器屢次操做 DOM 的成本。這一過程,官方起名 reconciliation,可翻譯爲協調算法
。可是 react 發展到今日,隨着前端應用的量級愈來愈大,reconciliation 已經日顯疲憊,React Fiber 應運而出。React Fiber 是對 React 核心算法的重寫,由 React 團隊歷時兩年多完成。html
當時被你們拍手叫好的 VDOM,爲何今日會略顯疲態,這還要從它的工做原理提及。在 react 發佈之初,設想將來的 UI 渲染會是異步的,從 setState()
的設計和 react 內部的事務機制能夠看出這點。在 react@16 之前的版本,reconciler(現被稱爲 stack reconciler )採用自頂向下遞歸,從根組件或 setState()
後的組件開始,更新整個子樹。若是組件樹不大不會有問題,可是當組件樹愈來愈大,遞歸遍歷的成本就越高,持續佔用主線程,這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,形成頓卡的視覺效果。前端
理論上人眼最高能識別的幀數不超過 30 幀,電影的幀數大多固定在 24,瀏覽器最優的幀率是 60,即16.5ms 左右渲染一次。 瀏覽器正常的工做流程應該是這樣的,運算 -> 渲染 -> 運算 -> 渲染 -> 運算 -> 渲染 …node
可是當 JS 執行時間過長,就變成了這個樣子,FPS(每秒顯示幀數)降低形成視覺上的頓卡。react
那麼這個問題如何解決,這就是 fiber reconciler 要作的事了。簡而言之能夠看下圖,將要執行的 JS 作拆分,保證不會阻塞主線程(Main thread)便可。git
將同步任務拆分你們都能理解,但在拆分以前咱們面臨如下幾個問題:github
# React@15
DOM 真實DOM節點
-------
Instances React 維護的 VDOM tree node
-------
Elements 描述 UI 長什麼樣子(type, props)
複製代碼
在 react@15 中,更新主要分爲兩個步驟完成: 1. diff diff 的實際工做是對比 prevInstance 和 nextInstance 的狀態,找出差別及其對應的 VDOM change。diff 本質上是一些計算(遍歷、比較),是可拆分的(算一半待會兒接着算)。 2. patch 將 diff 算法計算出來的差別隊列更新到真實的 DOM 節點上。React 並非計算出一個差別就執行一次 patch,而是計算出所有的差別並放入差別隊列後,再一次性的去執行 patch 方法完成真實的DOM更新。算法
最後的 patch 階段更新,是一連串的 DOM 操做,雖然能夠根據 diff 後獲得的 change list 作拆分,可是意義不大,不只會致使內部維護的 DOM 狀態和實際的不一致,也會影響體驗,因此應該作的是對 diff 階段進行拆分。從下圖是 ReactDOM 渲染 10000 個子組件的過程。能夠看到,在 diff 執行階段主線程一直被佔用,沒法進行其餘任何操做 I/O 操做,直到運行完成。瀏覽器
由此引出了 React Fiber 的解決方案,以一個 fiber 爲單位來進行拆分,fiber tree 是根據 VDOM tree 構造出來的,樹形結構徹底一致,只是包含的信息不一樣。如下是 fiber tree 節點的部分結構:bash
{
alternate: Fiber|null, // 在fiber更新時克隆出的鏡像fiber,對fiber的修改會標記在這個fiber上
nextEffect: Fiber | null, // 單鏈表結構,方便遍歷 Fiber Tree 上有反作用的節點
pendingWorkPriority: PriorityLevel, // 標記子樹上待更新任務的優先級
stateNode: any, // 管理 instance 自身的特性
return: Fiber|null, // 指向 Fiber Tree 中的父節點
child: Fiber|null, // 指向第一個子節點
sibling: Fiber|null, // 指向兄弟節點
}
複製代碼
Fiber 依次經過 return、child 及 sibling 的順序對 ReactElement 作處理,將以前簡單的樹結構,變成了基於單鏈表的樹結構,維護了更多的節點關係。架構
Stack 在執行時是以一個 tree 爲單位處理;Fiber 則是以一個 fiber 的單位執行。Stack 只能同步的執行;Fiber 則能夠針對該 Fiber 作調度處理。也就是說,假設如今有個 Fiber 其單鏈表(Linked List)結構爲 A → B → C,當 A 執行到 B 被中斷的話,能夠以後再次執行 B → C,這對 Stack 的同步處理結構來講是很難作到的。
在 React Fiber 執行的過程當中,主要分爲兩個階段(phase):
第一個階段主要工做是自頂向下構建一顆完整的 Fiber Tree, 在 rerender 的過程當中,根據以前生成的樹,構建名爲 workInProgress 的 Fiber Tree 用於更新操做。
假設我有上圖所示的 DOM 結構須要渲染,第一次 render 的時候會生成下圖所示的 Fiber Tree:
由於我須要對 Item 裏面的數值作平方運算,因而我點擊了 Button,react 根據以前生成的 Fiber Tree 開始構建workInProgress Tree。在構建的過程當中,以一個 fiber 節點爲單位自頂向下對比,若是發現根節點沒有發生改變,根據其 child 指針,把 List 節點複製到 workinprogress Tree 中。 每處理完一個 fiber 節點,react 都會檢查當前時間片是否夠用,若是發現當前時間片不夠用了,就是會標記下一個要處理的任務優先級,根據優先級來決定下一個時間片要處理什麼任務。
requestIdleCallback 會讓一個低優先級的任務在空閒期被調用,而 requestAnimationFrame 會讓一個高優先級的任務在下一個棧幀被調用,從而保證了主線程按照優先級執行 fiber 單元。 優先級順序爲:文本框輸入 > 本次調度結束需完成的任務 > 動畫過渡 > 交互反饋 > 數據更新 > 不會顯示但以防未來會顯示的任務。
module.exports = {
// heigh level
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs.
TaskPriority: 2, // Completes at the end of the current tick.
AnimationPriority: 3, // Needs to complete before the next frame.
// low level
HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 5, // Data fetching, or result from updating stores.
OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible. }; 複製代碼
在平方運算這一過程當中,react 經過依次對比 fiber 節點發現 List,Item2,Item3 發生了變化,就會在對應生成的 workInProgress Tree 中打一個 Tag,而且推送到 effect list 中。
當 reconciliation 結束後,根節點的 effect list 裏記錄了包括 DOM change 在內的全部 side effect,在第二階段(commit)執行更新操做,這樣一個流程就算結束了。
在這個示例中,詳細的比對流程並無細講,推薦觀看 Lin Clark 去年 react conf 中的演講,很是淺顯易懂,本文中示例也來自這個演講。
will
生命週期可能被屢次調用而影響性能。react 團隊給了咱們很長一段時間來處理這個問題,官方也提供了不少參考案例,能夠平滑過渡到下個版本。react@16 與其說是一個分水嶺,不如說是一個過渡,作的不少工做都是在給用戶打預防針,告訴你接下來該怎麼作,react@17纔會是掀起風浪的那一個。reconciliation 的重寫給 react 的將來帶來太多的可能,包括最近社區討論的如火如荼的 Hooks,其實也是 Fiber 帶來一種可能性。在後續的版本中,我的覺得寫法上會有不小的改變,主要是爲了更加優秀的性能服務;還有就是將一些社區產生的方案作優化,讓寫法更加人性化(HOC 中的 refs 以及 context 傳遞),以及對常見的問題給出官方的解決方案(異步數據處理)等等。除了優勢,固然也會帶來些問題。隨着版本的迭代,react 中的概念愈來愈多,新手學習的曲線只怕是會愈來愈陡峭。
在處理大型應用時,react 的表現不盡人意。主要緣由在於計算耗時太長,致使主線程一直被佔用,沒法處理其餘任務。react 團隊爲了解決這個問題,提出了 Fiber reconciliation 的方案來代替以前的 Stack reconciliation。Fiber 相較於 Stack,採用了異步的方式將以前同步執行的計算過程作拆分,使得主線程不會一直處於被佔用的狀態,能夠有時間去處理其餘任務,好比 I/O 操做,交互反饋等。