# 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 存起來的,資料結構如下:

### :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