[TOC]
# 介紹
在詳細介紹 Fiber 之前,先了解一下 Fiber 是什么,以及為什么 React 團隊要話兩年時間重構協調算法。
## React 的核心思想
**內存中維護一顆虛擬DOM樹,數據變化時(setState),自動更新虛擬 DOM,得到一顆新樹,然后 Diff 新老虛擬 DOM 樹,找到有變化的部分,得到一個 Change(Patch),將這個 Patch 加入隊列,最終批量更新這些 Patch 到 DOM 中**。
## React 16 之前的不足
首先我們了解一下 React 的工作過程,當我們通過`render()`和 `setState()` 進行組件渲染和更新的時候,React 主要有兩個階段:

**調和階段(Reconciler):**[官方解釋](https://link.juejin.cn?target=https%3A%2F%2Fzh-hans.reactjs.org%2Fdocs%2Freconciliation.html "https://zh-hans.reactjs.org/docs/reconciliation.html")。React 會自頂向下通過遞歸,遍歷新數據生成新的 Virtual DOM,然后通過 Diff 算法,找到需要變更的元素(Patch),放到更新隊列里面去。
**渲染階段(Renderer)**:遍歷更新隊列,通過調用宿主環境的API,實際更新渲染對應元素。宿主環境,比如 DOM、Native、WebGL 等。
在協調階段階段,由于是采用的遞歸的遍歷方式,這種也被成為 **Stack Reconciler**,主要是為了區別 **Fiber Reconciler** 取的一個名字。這種方式有一個特點:一旦任務開始進行,就**無法中斷**,那么 js 將一直占用主線程, 一直要等到整棵 Virtual DOM 樹計算完成之后,才能把執行權交給渲染引擎,那么這就會導致一些用戶交互、動畫等任務無法立即得到處理,就會有卡頓,非常的影響用戶體驗。
## 如何解決之前的不足
> 之前的問題主要的問題是任務一旦執行,就無法中斷,js 線程一直占用主線程,導致卡頓。
可能有些接觸前端不久的不是特別理解上面為什么 js 一直占用主線程就會卡頓,我這里還是簡單的普及一下。
### 瀏覽器每一幀都需要完成哪些工作?
頁面是一幀一幀繪制出來的,當每秒繪制的幀數(FPS)達到 60 時,頁面是流暢的,小于這個值時,用戶會感覺到卡頓。
1s 60 幀,所以每一幀分到的時間是 1000/60 ≈ 16 ms。所以我們書寫代碼時力求不讓一幀的工作量超過 16ms。

*瀏覽器一幀內的工作*
通過上圖可看到,一幀內需要完成如下六個步驟的任務:
* 處理用戶的交互
* JS 解析執行
* 幀開始。窗口尺寸變更,頁面滾去等的處理
* rAF(requestAnimationFrame)
* 布局
* 繪制
如果這六個步驟中,任意一個步驟所占用的時間過長,總時間超過 16ms 了之后,用戶也許就能看到卡頓。
而在上一小節提到的**調和階段**花的時間過長,也就是 js 執行的時間過長,那么就有可能在用戶有交互的時候,本來應該是渲染下一幀了,但是在當前一幀里還在執行 JS,就導致用戶交互不能麻煩得到反饋,從而產生卡頓感。
### 解決方案
**把渲染更新過程拆分成多個子任務,每次只做一小部分,做完看是否還有剩余時間,如果有繼續下一個任務;如果沒有,掛起當前任務,將時間控制權交給主線程,等主線程不忙的時候在繼續執行。** 這種策略叫做 [Cooperative Scheduling(合作式調度)](https://link.juejin.cn?target=https%3A%2F%2Fwww.w3.org%2FTR%2Frequestidlecallback%2F "https://www.w3.org/TR/requestidlecallback/"),操作系統常用任務調度策略之一。
> **補充知識**,操作系統常用任務調度策略:先來先服務(FCFS)調度算法、短作業(進程)優先調度算法(SJ/PF)、最高優先權優先調度算法(FPF)、高響應比優先調度算法(HRN)、時間片輪轉法(RR)、多級隊列反饋法。
合作式調度主要就是用來分配任務的,當有更新任務來的時候,不會馬上去做 Diff 操作,而是先把當前的更新送入一個 Update Queue 中,然后交給 **Scheduler** 去處理,Scheduler 會根據當前主線程的使用情況去處理這次 Update。為了實現這種特性,使用了`requestIdelCallback`API。對于不支持這個API 的瀏覽器,React 會加上 pollyfill。
在上面我們已經知道瀏覽器是一幀一幀執行的,在兩個執行幀之間,主線程通常會有一小段空閑時間,`requestIdleCallback`可以在這個**空閑期(Idle Period)調用空閑期回調(Idle Callback)**,執行一些任務。

* 低優先級任務由`requestIdleCallback`處理;
* 高優先級任務,如動畫相關的由`requestAnimationFrame`處理;
* `requestIdleCallback`可以在多個空閑期調用空閑期回調,執行任務;
* `requestIdleCallback`方法提供 deadline,即任務執行限制時間,以切分任務,避免長時間執行,阻塞UI渲染而導致掉幀;
這個方案看似確實不錯,但是怎么實現可能會遇到幾個問題:
* 如何拆分成子任務?
* 一個子任務多大合適?
* 怎么判斷是否還有剩余時間?
* 有剩余時間怎么去調度應該執行哪一個任務?
* 沒有剩余時間之前的任務怎么辦?
接下里整個 Fiber 架構就是來解決這些問題的。
# 什么是 Fiber
為了解決之前提到解決方案遇到的問題,提出了以下幾個目標:
* 暫停工作,稍后再回來。
* 為不同類型的工作分配優先權。
* 重用以前完成的工作。
* 如果不再需要,則中止工作。
為了做到這些,我們首先需要一種方法將任務分解為單元。從某種意義上說,這就是 Fiber,Fiber 代表一種**工作單元**。
但是僅僅是分解為單元也無法做到中斷任務,因為函數調用棧就是這樣,每個函數為一個工作,每個工作被稱為**堆棧幀**,它會一直工作,直到堆棧為空,無法中斷。
所以我們需要一種增量渲染的調度,那么就需要重新實現一個堆棧幀的調度,這個堆棧幀可以按照自己的調度算法執行他們。另外由于這些堆棧是可以自己控制的,所以可以加入并發或者錯誤邊界等功能。
因此 Fiber 就是重新實現的堆棧幀,本質上 Fiber 也可以理解為是一個**虛擬的堆棧幀**,將可中斷的任務拆分成多個子任務,通過按照優先級來自由調度子任務,分段更新,從而將之前的同步渲染改為異步渲染。
所以我們可以說 Fiber 是一種數據結構(堆棧幀),也可以說是一種解決可中斷的調用任務的一種解決方案,它的特性就是**時間分片(time slicing)**和**暫停(supense)**。
> 如果了解**協程**的可能會覺得 Fiber 的這種解決方案,跟協程有點像(區別還是很大的),是可以中斷的,可以控制執行順序。在 JS 里的 generator 其實就是一種協程的使用方式,不過顆粒度更小,可以控制函數里面的代碼調用的順序,也可以中斷。
# Fiber 是如何工作的
1. `ReactDOM.render()` 和 `setState` 的時候開始創建更新。
2. 將創建的更新加入任務隊列,等待調度。
3. 在 requestIdleCallback 空閑時執行任務。
4. 從根節點開始遍歷 Fiber Node,并且構建 WokeInProgress Tree。
5. 生成 effectList。
6. 根據 EffectList 更新 DOM。
下面是一個詳細的執行過程圖:

1. 第一部分從 `ReactDOM.render()` 方法開始,把接收的 React Element 轉換為 Fiber 節點,并為其設置優先級,創建 Update,加入到更新隊列,這部分主要是做一些初始數據的準備。
2. 第二部分主要是三個函數:`scheduleWork`、`requestWork`、`performWork`,即安排工作、申請工作、正式工作三部曲,React 16 新增的異步調用的功能則在這部分實現,這部分就是 **Schedule 階段**,前面介紹的 Cooperative Scheduling 就是在這個階段,只有在這個解決獲取到可執行的時間片,第三部分才會繼續執行。具體是如何調度的,后面文章再介紹,這是 React 調度的關鍵過程。
3. 第三部分是一個大循環,遍歷所有的 Fiber 節點,通過 Diff 算法計算所有更新工作,產出 EffectList 給到 commit 階段使用,這部分的核心是 beginWork 函數,這部分基本就是 **Fiber Reconciler ,包括 reconciliation 和 commit 階段**。
## Fiber Node
FIber Node,承載了非常關鍵的上下文信息,可以說是貫徹整個創建和更新的流程,下來分組列了一些重要的 Fiber 字段。
~~~
{
...
// 跟當前Fiber相關本地狀態(比如瀏覽器環境就是DOM節點)
stateNode: any,
// 單鏈表樹結構
return: Fiber | null,// 指向他在Fiber節點樹中的`parent`,用來在處理完這個節點之后向上返回
child: Fiber | null,// 指向自己的第一個子節點
sibling: Fiber | null, // 指向自己的兄弟結構,兄弟節點的return指向同一個父節點
// 更新相關
pendingProps: any, // 新的變動帶來的新的props
memoizedProps: any, // 上一次渲染完成之后的props
updateQueue: UpdateQueue<any> | null, // 該Fiber對應的組件產生的Update會存放在這個隊列里面
memoizedState: any, // 上一次渲染的時候的state
// Scheduler 相關
expirationTime: ExpirationTime, // 代表任務在未來的哪個時間點應該被完成,不包括他的子樹產生的任務
// 快速確定子樹中是否有不在等待的變化
childExpirationTime: ExpirationTime,
// 在Fiber樹更新的過程中,每個Fiber都會有一個跟其對應的Fiber
// 我們稱他為`current <==> workInProgress`
// 在渲染完成之后他們會交換位置
alternate: Fiber | null,
// Effect 相關的
effectTag: SideEffectTag, // 用來記錄Side Effect
nextEffect: Fiber | null, // 單鏈表用來快速查找下一個side effect
firstEffect: Fiber | null, // 子樹中第一個side effect
lastEffect: Fiber | null, // 子樹中最后一個side effect
....
};
復制代碼
~~~
## Fiber Reconciler
在第二部分,進行 Schedule 完,獲取到時間片之后,就開始進行 reconcile。
Fiber Reconciler 是 React 里的調和器,這也是任務調度完成之后,如何去執行每個任務,如何去更新每一個節點的過程,對應上面的第三部分。
reconcile 過程分為2個階段(phase):
1. (可中斷)render/reconciliation 通過構造 WorkInProgress Tree 得出 Change。
2. (不可中斷)commit 應用這些DOM change。
### reconciliation 階段
在 reconciliation 階段的每個工作循環中,每次處理一個 Fiber,處理完可以中斷/掛起整個工作循環。通過每個節點更新結束時向上歸并 **Effect List** 來收集任務結果,reconciliation 結束后,**根節點**的 Effect List里記錄了包括 DOM change 在內的所有 **Side Effect**。
render 階段可以理解為就是 Diff 的過程,得出 Change(Effect List),會執行聲明如下的聲明周期方法:
* \[UNSAFE\_\]componentWillMount(棄用)
* \[UNSAFE\_\]componentWillReceiveProps(棄用)
* getDerivedStateFromProps
* shouldComponentUpdate
* \[UNSAFE\_\]componentWillUpdate(棄用)
* render
由于 reconciliation 階段是可中斷的,一旦中斷之后恢復的時候又會重新執行,所以很可能 reconciliation 階段的生命周期方法會被多次調用,所以在 reconciliation 階段的生命周期的方法是不穩定的,我想這也是 React 為什么要廢棄 `componentWillMount` 和 `componentWillReceiveProps`方法而改為靜態方法 `getDerivedStateFromProps` 的原因吧。
### commit 階段
commit 階段可以理解為就是將 Diff 的結果反映到真實 DOM 的過程。
在 commit 階段,在 commitRoot 里會根據 `effect`的 `effectTag`,具體 effectTag 見[源碼](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Ffacebook%2Freact%2Fblob%2F504576306461a5ff339dc99691842f0f35a8bf4c%2Fpackages%2Fshared%2FReactSideEffectTags.js "https://github.com/facebook/react/blob/504576306461a5ff339dc99691842f0f35a8bf4c/packages/shared/ReactSideEffectTags.js") ,進行對應的插入、更新、刪除操作,根據 `tag` 不同,調用不同的更新方法。
commit 階段會執行如下的聲明周期方法:
* getSnapshotBeforeUpdate
* componentDidMount
* componentDidUpdate
* componentWillUnmount
> P.S:注意區別 reconciler、reconcile 和 reconciliation,reconciler 是調和器,是一個名詞,可以說是 React 工作的一個模塊,協調模塊;reconcile 是調和器調和的動作,是一個動詞;而 reconciliation 只是 reconcile 過程的第一個階段。
## Fiber Tree 和 WorkInProgress Tree
React 在 render 第一次渲染時,會通過 React.createElement 創建一顆 Element 樹,可以稱之為 **Virtual DOM Tree**,由于要記錄上下文信息,加入了 Fiber,每一個 Element 會對應一個 Fiber Node,將 Fiber Node 鏈接起來的結構成為 **Fiber Tree**。它反映了用于渲染 UI 的應用程序的狀態。這棵樹通常被稱為 **current 樹(當前樹,記錄當前頁面的狀態)。**
在后續的更新過程中(setState),每次重新渲染都會重新創建 Element, 但是 Fiber 不會,Fiber 只會使用對應的 Element 中的數據來更新自己必要的屬性,
Fiber Tree 一個重要的特點是鏈表結構,將遞歸遍歷編程循環遍歷,然后配合 requestIdleCallback API, 實現任務拆分、中斷與恢復。
這個鏈接的結構是怎么構成的呢,這就要主要到之前 Fiber Node 的節點的這幾個字段:
~~~
// 單鏈表樹結構
{
return: Fiber | null, // 指向父節點
child: Fiber | null,// 指向自己的第一個子節點
sibling: Fiber | null,// 指向自己的兄弟結構,兄弟節點的return指向同一個父節點
}
~~~
每一個 Fiber Node 節點與 Virtual Dom 一一對應,所有 Fiber Node 連接起來形成 Fiber tree, 是個單鏈表樹結構,如下圖所示:

對照圖來看,是不是可以知道 Fiber Node 是如何聯系起來的呢,Fiber Tree 就是這樣一個單鏈表。
**當 render 的時候有了這么一條單鏈表,當調用 `setState` 的時候又是如何 Diff 得到 change 的呢?**
采用的是一種叫**雙緩沖技術(double buffering)**,這個時候就需要另外一顆樹:WorkInProgress Tree,它反映了要刷新到屏幕的未來狀態。
WorkInProgress Tree 構造完畢,得到的就是新的 Fiber Tree,然后喜新厭舊(把 current 指針指向WorkInProgress Tree,丟掉舊的 Fiber Tree)就好了。
這樣做的好處:
* 能夠復用內部對象(fiber)
* 節省內存分配、GC的時間開銷
* 就算運行中有錯誤,也不會影響 View 上的數據
每個 Fiber上都有個`alternate`屬性,也指向一個 Fiber,創建 WorkInProgress 節點時優先取`alternate`,沒有的話就創建一個。
創建 WorkInProgress Tree 的過程也是一個 Diff 的過程,Diff 完成之后會生成一個 Effect List,這個 Effect List 就是最終 Commit 階段用來處理副作用的階段。
<br>
<br>
# 參考資料
* [Deep In React 之淺談 React Fiber 架構(一)](https://juejin.cn/post/6844903874692661255)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼