浅析 React Fiber
引言
在 react 進入大家視野之初,Virtual DOM(VDOM)的概念讓人眼前一亮,在操作真正的 DOM 之前,先通過 VDOM 前后對比得出需要更新的部分,再去操作真實的 DOM,減少了瀏覽器多次操作 DOM 的成本。這一過程,官方起名 reconciliation,可翻譯為協調算法。但是 react 發展到今日,隨著前端應用的量級越來越大,reconciliation 已經日顯疲憊,React Fiber 應運而出。React Fiber 是對 React 核心算法的重寫,由 React 團隊歷時兩年多完成。
動機
當時被大家拍手叫好的 VDOM,為什么今日會略顯疲態,這還要從它的工作原理說起。在 react 發布之初,設想未來的 UI 渲染會是異步的,從 setState() 的設計和 react 內部的事務機制可以看出這點。在 react@16 以前的版本,reconciler(現被稱為 stack reconciler )采用自頂向下遞歸,從根組件或 setState() 后的組件開始,更新整個子樹。如果組件樹不大不會有問題,但是當組件樹越來越大,遞歸遍歷的成本就越高,持續占用主線程,這樣主線程上的布局、動畫等周期性任務以及交互響應就無法立即得到處理,造成頓卡的視覺效果。
理論上人眼最高能識別的幀數不超過 30 幀,電影的幀數大多固定在 24,瀏覽器最優的幀率是 60,即16.5ms 左右渲染一次。 瀏覽器正常的工作流程應該是這樣的,運算 -> 渲染 -> 運算 -> 渲染 -> 運算 -> 渲染 …
但是當 JS 執行時間過長,就變成了這個樣子,FPS(每秒顯示幀數)下降造成視覺上的頓卡。
那么這個問題如何解決,這就是 fiber reconciler 要做的事了。簡而言之可以看下圖,將要執行的 JS 做拆分,保證不會阻塞主線程(Main thread)即可。
工作原理
將同步任務拆分大家都能理解,但在拆分之前我們面臨以下幾個問題:
- 拆什么?
- 如何拆?
- 拆分后的執行順序如何?
拆什么
# 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 個子組件的過程??梢钥吹?#xff0c;在 diff 執行階段主線程一直被占用,無法進行其他任何操作 I/O 操作,直到運行完成。
怎么拆
由此引出了 React Fiber 的解決方案,以一個 fiber 為單位來進行拆分,fiber tree 是根據 VDOM tree 構造出來的,樹形結構完全一致,只是包含的信息不同。以下是 fiber tree 節點的部分結構:
{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 levelNoWork: 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 levelHighPriority: 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 中的演講,非常淺顯易懂,本文中示例也來自這個演講。
暢想未來
- 異步渲染 今年在冰島舉行的 JS Conf,Dan 提到了異步渲染的概念,異步渲染不是說一個個加載組件,而是說在以異步的方式加載的同時給人以同步流程的體驗,在老設備上,通過犧牲一些加載時間來獲得一種流暢的體驗。其實在 React@16 版本中,異步渲染默認是關閉的,雖然可以通過 hack 的方式實現非同步,但是因為沒有寫測試,還是會有 BUG 存在。
- 生命周期大換血 在 react@16 版本中,雖然依舊支持之前的生命周期函數,但是官方已經說明在下個版本中會將廢棄其中的部分,這么做的原因,主要是 reconciliation 的重寫導致。在 render/reconciliation 的過程中,因為存在優先級和時間片的概念,一個任務很可能執行到一半就被其他優先級更高的任務所替代,或者因為時間原因而被終止。當再次執行這個任務時,是從頭開始執行一遍,就會導致組件的某些 will 生命周期可能被多次調用而影響性能。react 團隊給了我們很長一段時間來處理這個問題,官方也提供了很多參考案例,可以平滑過渡到下個版本。
react@16 與其說是一個分水嶺,不如說是一個過渡,做的很多工作都是在給用戶打預防針,告訴你接下來該怎么做,react@17才會是掀起風浪的那一個。reconciliation 的重寫給 react 的未來帶來太多的可能,包括最近社區討論的如火如荼的 Hooks,其實也是 Fiber 帶來一種可能性。在后續的版本中,個人以為寫法上會有不小的改變,主要是為了更加優秀的性能服務;還有就是將一些社區產生的方案做優化,讓寫法更加人性化(HOC 中的 refs 以及 context 傳遞),以及對常見的問題給出官方的解決方案(異步數據處理)等等。除了優點,當然也會帶來些問題。隨著版本的迭代,react 中的概念越來越多,新手學習的曲線只怕是會越來越陡峭。
總結
在處理大型應用時,react 的表現不盡人意。主要原因在于計算耗時太長,導致主線程一直被占用,無法處理其他任務。react 團隊為了解決這個問題,提出了 Fiber reconciliation 的方案來代替之前的 Stack reconciliation。Fiber 相較于 Stack,采用了異步的方式將之前同步執行的計算過程做拆分,使得主線程不會一直處于被占用的狀態,可以有時間去處理其他任務,比如 I/O 操作,交互反饋等。
參考文獻
- 如何理解 React Fiber 架構? - 知乎
- React Fiber - 掘金
- 圖解瀏覽器的基本工作原理 - 知乎
- React 16 Fiber源碼速覽 | Zindex’s blog
- 翻譯 React Fiber 現狀確認 – CYB – Medium
- 完全理解React Fiber | 黯羽輕揚
- A Cartoon Intro to Fiber
- blog/from-jsx-to-dom.md at master · xieyu/blog · GitHub
- Sneak Peek: Beyond React 16
總結
以上是生活随笔為你收集整理的浅析 React Fiber的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sw怎么生成齿条? solidworks
- 下一篇: VanMoof 在荷兰宣布破产,曾自称“