# React Fiber [TOC] ## :whale: Why Use Circular Queue? 上次讀書會的疑問,想了一下,用 circular queue 的好處,目前有想到以下幾個: 1. efficient memory management 2. efficient search 強型別的語言在一開始宣告變數的時候,就會宣告 array 的大小,所以 array 大小是固定的,用 circular array 可以增加記憶體使用的效率。 用 circular queue 在 search queue 裡面的資料時會比不 circular 的 queue 好一點點。 ## :whale: 一些名詞解釋 - Reconciliation: react diff 的演算法 - fiber: react diff 的演算法實作(可以有很多不同的實作,只是 react 16 之後是用 fiber) - render phase, commit phase: react 把繪製 DOM 的流程抽象成兩個階段,第一個階段是找出需要重繪的 DOM,第二的階段是把 DOM 繪製到瀏覽器上,這樣的抽象化讓第二階段可以在不同的裝置、載體或是 renderer 上繪製 DOM,例如: react-native, react-three-fiber ## :whale: Why Fiber? ### :crab: React 16 之前的架構 ```mermaid flowchart TD A[Stack Reconciler] --> B[Renderer] ``` - Stack Reconciler:負責找出有變化的 component - Renderer:負責將變化的 component 渲染到頁面上 Reconciler 是採取遞迴的策略去找出有變化的 component,這種策略會有以下的問題: 遞迴一但開始就會將 function 一直放進 main thread 裡面執行,直到結束為止。這樣會導致瀏覽器的主執行序都在執行遞迴,如果遞迴的層級過深,有可能會導致畫面掉幀、卡頓,影響到用戶的使用體驗。 可以參考以下遞迴範例: ```mermaid flowchart TD A(A1) --> B(B1) A --> C(B2) B --> D(C1) B --> E(C2) C --> F(C3) C --> G(C4) ``` > Traversal Sequence: A1 B1 C1 C2 B2 C3 C4 ```js= 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); ``` 於是 React Core Team 提出了新的 react fiber 架構。 看到覺得不錯的 blog 文章,解釋了為什麼耗時的 JS 會導致瀏覽器掉幀:[浏览器的 16ms 渲染帧 | Harttle Land](https://harttle.land/2017/08/15/browser-render-frame.html) --- #### 為什麼遞迴會導致瀏覽器掉幀? 遞迴可能導致瀏覽器掉幀的主要原因是: 1. 執行時間過長: 深度遞迴可能需要大量的計算時間,占用主線程,導致瀏覽器無法及時響應用戶操作和更新畫面。 2. 堆棧溢出: 過深的遞迴調用會導致調用堆棧持續增長,最終可能超出瀏覽器的限制而崩潰。 3. 內存消耗: 每次遞迴調用都會在內存中創建新的函數上下文,大量遞迴可能導致內存使用過高。 阻塞渲染: JavaScript是單線程執行的,長時間的遞迴計算會阻塞UI渲染線程。 為避免這些問題,可以考慮以下方法: 1. 使用迭代代替遞迴 2. 實現尾遞迴優化 3. 將大型遞迴任務拆分,使用 setTimeout 或 requestAnimationFrame 來分批執行 4. 利用 Web Workers 將複雜計算放在後台線程中執行 #### 執行時間過長這個部分,瀏覽器不是可以在不同的幀執行 function 嗎?遞迴也是很多 function,為什麼這些 function 不能在瀏覽器 idle 的時間執行?是有什麼機制嗎? 您提出了一個非常好的問題。確實,瀏覽器有能力在不同的幀中執行函數,但遞迴的特殊性質使得這個過程變得複雜。讓我解釋一下背後的機制: JavaScript 的執行模型: 1. JavaScript 在瀏覽器中是單線程執行的,使用一個事件循環(Event Loop)來處理任務。 2. 函數調用棧: 當一個函數被調用時,它會被推入調用棧。遞迴函數會不斷地將自己推入棧中,直到達到基本情況。 3 .阻塞特性: 一旦一個函數開始執行,它會一直執行到完成,除非顯式地讓出控制權。遞迴函數在達到基本情況之前不會自然地讓出控制權。 4. 任務切分的困難: 瀏覽器確實可以在空閒時間執行任務,但遞迴函數通常是一個連續的過程,不易在中間被打斷或分割成多個獨立的任務。 5. 調度機制: 瀏覽器的任務調度機制主要是針對異步操作和宏任務設計的,而不是為了處理單個長時間運行的同步函數(如遞迴)。 要讓遞迴函數能夠在不同的幀中執行,需要手動將其改造成可中斷的形式。例如: ```js= function recursiveTask(n) { if (n <= 0) return; // 執行一些工作 console.log(n); // 使用 setTimeout 來調度下一次遞迴 setTimeout(() => recursiveTask(n - 1), 0); } recursiveTask(1000); ``` 這種方法將遞迴轉換為一系列的異步操作,允許瀏覽器在每次遞迴之間執行其他任務,從而避免長時間阻塞主線程。 總的來說,遞迴導致瀏覽器掉幀的問題主要是由於其連續執行的特性,而不是瀏覽器缺乏在空閒時間執行函數的能力。改造遞迴以適應瀏覽器的異步執行模型是解決這個問題的一種方法。 --- ### :crab: React 16 之後的架構 ```mermaid flowchart TD C[Scheduler] --> A A[Fiber Reconciler] --> B[Renderer] ``` React 16 之後發想了一個稱為 React Fiber 的架構。簡單來說,多新增了一個 Scheduler,讓這個 Scheduler 可以非同步的安排 Reconcilion 的任務,包括: - 中斷任務和恢復任務(pause work and come back to it later.) - 排定任務的優先級(assign priority to different types of work.) - 重複執行先前完成的任務(reuse previously completed work.) - 丟掉不需要的任務(abort work if it's no longer needed.) 而關於上面的任務安排的資訊,都會被存在 react fiber 當中。 實現的方式是用 React 團隊自己寫的 requestIdleCallback,因為瀏覽器的 requestAnimationFrame 和 requestIdleCallback 支援尚未完善。(待確認) ## :whale: React Fiber React Fiber 其實就是一個 Object,儲存了各種 render 需要的資訊,比如說這個 component 的 tag.用到的 state,這個 component 的 children 還有 render 的優先級等等。 ```json { type: any, // For a class component, it points to constructors; for a DOM element, it specifies HTML tags key: null | string, // The unique identifier stateNode: any, // Save the references to class instances of components, DOM nodes, or other React element types associated with the fiber node child: Fiber | null, // The first child node sibling: Fiber | null, // The next child node return: Fiber | null, // The parent node tag: WorkTag, // Define the type of fiber action. For more information,see https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js nextEffect: Fiber | null, // The pointer to next node updateQueue: mixed, // The queue for status update, callback function, and DOM update memoizedState: any, // The fiber state for output creation pendingProps: any, // The props that are updated from the new data of React elements and need to be applied to child components or DOM elements memoizedProps: any, // The props used to create the output during the previous rendering // …… } ``` React Fiber 詳細的 type 可以參考原始碼:https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js#L84 > Fiber is an unit of work. 蠻多文章都有提到 Fiber 可以想像成 react 的一個工作單位。我自己的理解是,因為新的架構有了 scheduler 的加入,讓 task 可以暫停、恢復、調整順序.所以才需要將 task 拆解解成更細的粒度(放棄最初用遞迴暴力執行的方式),因此才有了 Fiber 的設計。 所以平常開發者在設計 component 時的思考可能就蠻重要的,因為這關係到了工作單位的拆分,如果一個工作單位的職責越明確,除了符合 SOLID 原則之外,也會適當減少 react 不必要的工作,避免額外的資源開銷。 ### :crab: updateQueue ```js= class Update { constructor(payload, nextUpdate) { this.payload = payload // Payload data this.nextUpdate = nextUpdate // The pointer to the next unit } } class UpdateQueue { constructor() { this.baseState = null // state this.firstUpdate = null // The first update this.lastUpdate = null // The last update } enqueueUpdate(update) { // Current linked list is empty if (!this.firstUpdate) { this.firstUpdate = this.lastUpdate = update } else { // Current linked list is not empty this.lastUpdate.nextUpdate = update this.lastUpdate = update } } // Obtain the state, traverse the linked list, and update the result forceUpdate() { let currentState = this.baseState || {} let currentUpdate = this.firstUpdate while (currentUpdate) { // Determine whether it's a function or an object. If it is a function, execute it. Otherwise, return it directly let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload currentState = { ...currentState, ...nextState } currentUpdate = currentUpdate.nextUpdate } // Clear the linked list after the update is complete this.firstUpdate = this.lastUpdate = null this.baseState = currentState return currentState } } ``` ```js= 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); ``` ## :whale: React Fiber Node Tree fiber node tree 是用 linked list 存起來的,資料結構如下: ![f49c9f904ccf9c9da2022df52dac985da2028488](https://hackmd.io/_uploads/H1IxeyBYC.png) ### :crab: Current Tree & WorkInProgress Tree ### :crab: How to Traversal Fiber Node Tree? 改用 while,減少 call stack 裡面的 function 堆疊 ### :crab: Effects List ### :crab: How Reconciliation Work? #### Render Phase #### Commit Phase ## :whale: Resource - https://legacy.reactjs.org/docs/codebase-overview.html - https://legacy.reactjs.org/docs/implementation-notes.html - https://legacy.reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html - https://blog.ag-grid.com/inside-fiber-an-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/ - https://github.com/acdlite/react-fiber-architecture - https://www.alibabacloud.com/blog/a-closer-look-at-react-fiber_598138