走進(jìn)React Fiber 架構(gòu)

> 本文重點:介紹React重構(gòu)的起因和目的,理解Fiber tree單向鏈表結(jié)構(gòu)中各屬性含義,梳理調(diào)度過程和核心實現(xiàn)手段,深入新的生命周期,hooks,suspense,異常捕獲等特性的用法和原理。

> 喜歡的就點個贊吧?,希望跟大家在枯燥的源碼中發(fā)掘?qū)W習(xí)的樂趣,一起分享進(jìn)步。

當(dāng)react剛推出的時候,最具革命性的特性就是虛擬dom,因為這大大降低了應(yīng)用開發(fā)的難度,相比較以往告訴瀏覽器我需要怎么更新我的ui,現(xiàn)在我們只需要告訴react我應(yīng)用ui的下個狀態(tài)是怎么樣的,react會幫我們自動處理兩者之間的所有事宜。

這讓我們可以從屬性操作、事件處理和手動 DOM 更新這些在構(gòu)建應(yīng)用程序時必要的操作中解放出來。宿主樹的概念讓這個優(yōu)秀的框架有無限的可能性,react native便是其在原生移動應(yīng)用中偉大的實現(xiàn)。

但在享受舒適開發(fā)體驗的同時,有一些疑問一直縈繞在我們腦海中:

- 是什么導(dǎo)致了react用戶交互、動畫頻繁卡頓?

- 如何視線優(yōu)雅的異常處理,進(jìn)行異常捕獲和備用ui渲染?

- 如何更好實現(xiàn)組件的復(fù)用和狀態(tài)管理?

<!--- setState為啥是異步的-->

<!--react性能優(yōu)化何去何從? -->

這究竟是人性的扭曲,還是道德的淪喪 /狗頭

Fiber能否給我們答案,又將帶給我們什么驚喜,卷起一波新的浪潮,歡迎收看《走進(jìn)Fiber》

<!--是什么驅(qū)動react核心團(tuán)隊對其核心算法進(jìn)行重構(gòu)? -->

<!--他們又將有帶給我們什么驚喜,卷起一波新的浪潮呢?? -->

那么,簡而言之,React Fiber是什么?

>**Fiber**是對React核心算法的重構(gòu),2年重構(gòu)的產(chǎn)物就是Fiber reconciler。

## react協(xié)調(diào)是什么

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf21b454462?w=1202&h=740&f=png&s=93298)

>協(xié)調(diào)是react中重要的一部分,其中包含了如何對新舊樹差異進(jìn)行比較以達(dá)到僅更新差異的部分。?

現(xiàn)在的react經(jīng)過重構(gòu)后Reconciliation和Rendering被分為兩個不同的階段。?

- **reconciler協(xié)調(diào)階段**:當(dāng)組件次初始化和其后的狀態(tài)更新中,React會創(chuàng)建兩顆不相同的虛擬樹,React 需要基于這兩棵樹之間的差別來判斷如何有效率的更新 UI 以保證當(dāng)前 UI 與最新的樹保持同步,計算樹哪些部分需要更新。?

- **renderer階段**:渲染器負(fù)責(zé)將拿到的虛擬組件樹信息,根據(jù)其對應(yīng)環(huán)境真實地更新渲染到應(yīng)用中。有興趣的朋友可以看一下dan自己的博客中的文章=》[運行時的react=》渲染器](https://overreacted.io/react-as-a-ui-runtime/#renderers),介紹了react的Renderer渲染器如react-dom和react native等,其可以根據(jù)不同的主環(huán)境來生成不同的實例。

## 為什么要重寫協(xié)調(diào)

<!--線程:-->

<!--進(jìn)程:-->

<!--為什么是單線程:-->

> 動畫是指由許多幀靜止的畫面,以一定的速度(如每秒16張)連續(xù)播放時,肉眼因視覺殘象產(chǎn)生錯覺,而誤以為畫面活動的作品。——維基百科

老一輩人常常把電影稱為“移動的畫”,我們小時候看的手翻書就是快速翻動的一頁頁畫,其本質(zhì)上實現(xiàn)原理跟動畫是一樣的。

<!--<img src="https://ask.qcloudimg.com/http-save/yehe-2687933/l5f1eqnxee.gif" width="300">-->

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf284a718e2?w=500&h=211&f=gif&s=509016)

幀:在動畫過程中,每一幅靜止畫面即為一“幀”;?

幀率:是用于測量顯示幀數(shù)的量度,測量單位為“每秒顯示幀數(shù)”(Frame per Second,F(xiàn)PS)或“赫茲”;?

幀時長:即每一幅靜止畫面的停留時間,單位一般是ms(毫秒);?

丟幀:在幀率固定的動畫中,某一幀的時長遠(yuǎn)高于平均幀時長,導(dǎo)致其后續(xù)數(shù)幀被擠壓而丟失的現(xiàn)象;

當(dāng)前大部分筆記本電腦和手機(jī)的常見幀率為60hz,即一秒顯示60幀的畫面,一幀停留的時間為16.7ms(1000/60≈16.7),這就留給了開發(fā)者和UI系統(tǒng)大約16.67ms來完成生成一張靜態(tài)圖片(幀)所需要的所有工作。如果在這分派的16.67ms之內(nèi)沒有能夠完成這些工作,就會引發(fā)‘丟幀’的后果,使界面表現(xiàn)的不夠流暢。

瀏覽器中的GUI渲染線程和JS引擎線程

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf21f4a46c2?w=500&h=339&f=png&s=40774)

> 在瀏覽器中GUI渲染線程與JS引擎線程是互斥的,當(dāng)JS引擎執(zhí)行時GUI線程會被掛起(相當(dāng)于被凍結(jié)了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執(zhí)行。?

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf28320843d?w=720&h=450&f=jpeg&s=98584)

瀏覽器擁擠的主線程

React16 推出Fiber之前協(xié)調(diào)算法是Stack Reconciler,即遞歸遍歷所有的 Virtual DOM 節(jié)點執(zhí)行Diff算法,一旦開始便無法中斷,直到整顆虛擬dom樹構(gòu)建完成后才會釋放主線程,因其JavaScript單線程的特點,若當(dāng)下組件具有復(fù)雜的嵌套和邏輯處理,diff便會堵塞UI進(jìn)程,使動畫和交互等優(yōu)先級相對較高的任務(wù)無法立即得到處理,造成頁面卡頓掉幀,影響用戶體驗。

16年在 facebook 上 Seb 正式提到了 Fiber 這個概念,解釋為什么要重寫框架:

>Once you have each stack frame as an object on the heap you can do clever things like reusing it during future updates and yielding to the event loop without losing any of your currently in progress data.?

一旦將每個堆棧幀作為堆上的對象,您就可以做一些聰明的事情,例如在將來的更新中重用它并暫停于事件循環(huán),而不會丟失任何當(dāng)前正在進(jìn)行的數(shù)據(jù)。

我們來做一個實驗

```

function randomHexColor() {

? return (

? ? "#" + ("0000" + ((Math.random() * 0x1000000) << 0).toString(16)).substr(-6)

? );

}

var root = document.getElementById("root");

// 一次性遍歷100000次

function a() {

? setTimeout(function() {

? ? var k = 0;

? ? for (var i = 0; i < 10000; i++) {

? ? ? k += new Date() - 0;

? ? ? var el = document.createElement("div");

? ? ? el.innerHTML = k;

? ? ? root.appendChild(el);

? ? ? el.style.cssText = `background:${randomHexColor()};height:40px`;

? ? }

? }, 1000);

}

// 每次只操作100個節(jié)點,共100次

function b() {

? setTimeout(function() {

? ? function loop(n) {

? ? ? var k = 0;

? ? ? console.log(n);

? ? ? for (var i = 0; i < 100; i++) {

? ? ? ? k += new Date() - 0;

? ? ? ? var el = document.createElement("div");

? ? ? ? el.innerHTML = k;

? ? ? ? root.appendChild(el);

? ? ? ? el.style.cssText = `background:${randomHexColor()};height:40px`;

? ? ? }

? ? ? if (n) {

? ? ? ? setTimeout(function() {

? ? ? ? ? loop(n - 1);

? ? ? ? }, 40);

? ? ? }

? ? }

? ? loop(100);

? }, 1000);

}

```

a執(zhí)行性能截圖:掉幀嚴(yán)重,普遍fps為1139.6ms

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9c7784955307?w=1094&h=594&f=jpeg&s=112698)

b執(zhí)行性能截圖: fps處于15ms~19ms

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9d2b6204e156?w=1314&h=776&f=jpeg&s=225083)

>究其原因是因為瀏覽器的主線程需要處理GUI描繪,時間器處理,事件處理,JS執(zhí)行,遠(yuǎn)程資源加載等,當(dāng)做某件事,只有將它做完才能做下一件事。如果有足夠的時間,瀏覽器是會對我們的代碼進(jìn)行編譯優(yōu)化(JIT)及進(jìn)行熱代碼優(yōu)化,一些DOM操作,內(nèi)部也會對reflow進(jìn)行處理。reflow是一個性能黑洞,很可能讓頁面的大多數(shù)元素進(jìn)行重新布局。

而作為一只有夢想的前端菜??,為用戶爸爸呈現(xiàn)最好的交互體驗是我們義不容辭的責(zé)任,把困難扛在肩上,讓我們see see react是如何解決以上的問題。

## Fiber你是個啥(第四音

那么我們先看看作為看看解決方案的Fiber是什么,然后在分析為什么它能解決以上問題。

#### 定義:

1. react Reconciliation協(xié)調(diào)核心算法的一次重新實現(xiàn)

2. 虛擬堆棧幀

3. 具備扁平化的鏈表數(shù)據(jù)存儲結(jié)構(gòu)的js對象,Reconciliation階段所能拆分的最小工作單元

#### 針對其定義我們來進(jìn)行拓展:

##### 虛擬堆棧幀:?

Andrew Clark的[React Fiber體系文檔](https://github.com/acdlite/react-fiber-architecture)很好地解釋了Fiber實現(xiàn)背后的想法,我在這里引用一下:

> Fiber是堆棧的重新實現(xiàn),專門用于React組件。

您可以將單個Fiber視為虛擬堆??蚣?。

重新實現(xiàn)堆棧的優(yōu)點是,您可以將堆棧幀保留在內(nèi)存中,并根據(jù)需要(以及在任何時候)執(zhí)行它們。

這對于實現(xiàn)調(diào)度的目標(biāo)至關(guān)重要。

##### JavaScript的執(zhí)行模型:call stack

JavaScript原生的執(zhí)行模型:通過調(diào)用棧來管理函數(shù)執(zhí)行狀態(tài)。?

其中每個棧幀表示一個工作單元(a unit of work),存儲了函數(shù)調(diào)用的返回指針、當(dāng)前函數(shù)、調(diào)用參數(shù)、局部變量等信息。

因為JavaScript的執(zhí)行棧是由引擎管理的,執(zhí)行棧一旦開始,就會一直執(zhí)行,直到執(zhí)行棧清空。無法按需中止。

react以往的渲染就是使用原生執(zhí)行棧來管理組件樹的遞歸渲染,當(dāng)其層次較深component不斷遞歸子節(jié)點,無法被打斷就會導(dǎo)致主線程堵塞ui卡頓。

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf2280b9665?w=796&h=276&f=png&s=37426)

##### 可控的調(diào)用棧

所以理想狀況下reconciliation的過程應(yīng)該是像下圖所示一樣,將繁重的任務(wù)劃分成一個個小的工作單元,做完后能夠“喘口氣兒”。我們需要一種增量渲染的調(diào)度,F(xiàn)iber就是重新實現(xiàn)一個堆棧幀的調(diào)度,這個堆棧幀可以按照自己的調(diào)度算法執(zhí)行他們。另外由于這些堆棧是可將可中斷的任務(wù)拆分成多個子任務(wù),通過按照優(yōu)先級來自由調(diào)度子任務(wù),分段更新,從而將之前的同步渲染改為異步渲染。

它的特性就是時間分片(time slicing)和暫停(supense)。

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf22055a622?w=794&h=296&f=png&s=89423)

#### 具備扁平化的鏈表數(shù)據(jù)存儲結(jié)構(gòu)的js對象:

fiber是一個js對象,fiber的創(chuàng)建是通過React元素來創(chuàng)建的,在整個React構(gòu)建的虛擬DOM樹中,每一個元素都對應(yīng)有一個fiber,從而構(gòu)建了一棵fiber樹,每個fiber不僅僅包含每個元素的信息,還包含更多的信息,以方便Scheduler來進(jìn)行調(diào)度。

讓我們看一下fiber的結(jié)構(gòu)

```

type Fiber = {|

? // 標(biāo)記不同的組件類型

? //export const FunctionComponent = 0;

? //export const ClassComponent = 1;

? //export const HostRoot = 3; 可以理解為這個fiber是fiber樹的根節(jié)點,根節(jié)點可以嵌套在子樹中

? //export const Fragment = 7;

? //export const SuspenseComponent = 13;

? //export const MemoComponent = 14;

? //export const LazyComponent = 16;

? tag: WorkTag,

? // ReactElement里面的key

? // 唯一標(biāo)示。我們在寫React的時候如果出現(xiàn)列表式的時候,需要制定key,這key就是對應(yīng)元素的key。

? key: null | string,

? // ReactElement.type,也就是我們調(diào)用`createElement`的第一個參數(shù)

? elementType: any,

? // The resolved function/class/ associated with this fiber.

? // 異步組件resolved之后返回的內(nèi)容,一般是`function`或者`class`

? type: any,

? // The local state associated with this fiber.

? // 跟當(dāng)前Fiber相關(guān)本地狀態(tài)(比如瀏覽器環(huán)境就是DOM節(jié)點)

? // 當(dāng)前組件實例的引用

? stateNode: any,

? // 指向他在Fiber節(jié)點樹中的`parent`,用來在處理完這個節(jié)點之后向上返回

? return: Fiber | null,

? // 單鏈表樹結(jié)構(gòu)

? // 指向自己的第一個子節(jié)點

? child: Fiber | null,

? // 指向自己的兄弟結(jié)構(gòu)

? // 兄弟節(jié)點的return指向同一個父節(jié)點

? sibling: Fiber | null,

? index: number,

? // ref屬性

? ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

? // 新的變動帶來的新的props

? pendingProps: any,

? // 上一次渲染完成之后的props

? memoizedProps: any,

? // 該Fiber對應(yīng)的組件產(chǎn)生的Update會存放在這個隊列里面

? updateQueue: UpdateQueue<any> | null,

? // 上一次渲染的時候的state

? // 用來存放某個組件內(nèi)所有的 Hook 狀態(tài)

? memoizedState: any,

? // 一個列表,存放這個Fiber依賴的context

? firstContextDependency: ContextDependency<mixed> | null,

? // 用來描述當(dāng)前Fiber和他子樹的`Bitfield`

? // 共存的模式表示這個子樹是否默認(rèn)是異步渲染的

? // Fiber被創(chuàng)建的時候他會繼承父Fiber

? // 其他的標(biāo)識也可以在創(chuàng)建的時候被設(shè)置

? // 但是在創(chuàng)建之后不應(yīng)該再被修改,特別是他的子Fiber創(chuàng)建之前

? //用來描述fiber是處于何種模式。用二進(jìn)制位來表示(bitfield),后面通過與來看兩者是否相同//這個字段其實是一個數(shù)字.實現(xiàn)定義了一下四種//NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2//ProfileMode: 0b100->4

? mode: TypeOfMode,

? // Effect

? // 用來記錄Side Effect具體的執(zhí)行的工作的類型:比如Placement,Update等等

? effectTag: SideEffectTag,

? // 單鏈表用來快速查找下一個side effect

? nextEffect: Fiber | null,

? // 子樹中第一個side effect

? firstEffect: Fiber | null,

? // 子樹中最后一個side effect

? lastEffect: Fiber | null,

? // 代表任務(wù)在未來的哪個時間點應(yīng)該被完成

? // 不包括他的子樹產(chǎn)生的任務(wù)

? // 通過這個參數(shù)也可以知道是否還有等待暫停的變更、沒有完成變更。

? // 這個參數(shù)一般是UpdateQueue中最長過期時間的Update相同,如果有Update的話。

? expirationTime: ExpirationTime,

? // 快速確定子樹中是否有不在等待的變化

? childExpirationTime: ExpirationTime,

? //當(dāng)前fiber對應(yīng)的工作中的Fiber。

? // 在Fiber樹更新的過程中,每個Fiber都會有一個跟其對應(yīng)的Fiber

? // 我們稱他為 current <==> workInProgress

? // 在渲染完成之后他們會交換位置

? alternate: Fiber | null,

? ...

|};

```

[ReactWorkTags組件類型](https://github.com/facebook/react/blob/master/packages/shared/ReactWorkTags.js)

### 鏈表結(jié)構(gòu)

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf2a8d923cc?w=644&h=522&f=jpeg&s=40493)

fiber中最為重要的是return、child、sibling指針,連接父子兄弟節(jié)點以構(gòu)成一顆單鏈表fiber樹,其扁平化的單鏈表結(jié)構(gòu)的特點將以往遞歸遍歷改為了循環(huán)遍歷,實現(xiàn)深度優(yōu)先遍歷。<!--三個屬性串聯(lián)了整個應(yīng)用,能夠以很高的效率把整個應(yīng)用遍歷完。-->

<!--在任何時候一個組件實例只有兩個Fiber=>current和workinprogress。-->

React16特別青睞于鏈表結(jié)構(gòu),鏈表在內(nèi)存里不是連續(xù)的,動態(tài)分配,增刪方便,輕量化,對異步友好

<!--![image](http://pz5ikgh3i.bkt.clouddn.com/20190128102004436.png)-->

###? current與workInProgress

**current樹**:React 在 render 第一次渲染時,會通過 React.createElement 創(chuàng)建一顆 Element 樹,可以稱之為 Virtual DOM Tree,由于要記錄上下文信息,加入了 Fiber,每一個 Element 會對應(yīng)一個 Fiber Node,將 Fiber Node 鏈接起來的結(jié)構(gòu)成為 Fiber Tree。它反映了用于渲染 UI 和映射應(yīng)用狀態(tài)。這棵樹通常被稱為 current 樹(當(dāng)前樹,記錄當(dāng)前頁面的狀態(tài))。

**workInProgress樹**:當(dāng)React經(jīng)過current當(dāng)前樹時,對于每一個先存在的fiber節(jié)點,它都會創(chuàng)建一個替代(alternate)節(jié)點,這些節(jié)點組成了workInProgress樹。這個節(jié)點是使用render方法返回的React元素的數(shù)據(jù)創(chuàng)建的。一旦更新處理完以及所有相關(guān)工作完成,React就有一顆替代樹來準(zhǔn)備刷新屏幕。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當(dāng)前樹。下次進(jìn)來會把current狀態(tài)復(fù)制到WIP上,進(jìn)行交互復(fù)用,而不用每次更新的時候都創(chuàng)建一個新的對象,消耗性能。這種同時緩存兩棵樹進(jìn)行引用替換的技術(shù)被稱為**雙緩沖技術(shù)**。

```

function createWorkInProgress(current, ...) {

? let workInProgress = current.alternate;

? if (workInProgress === null) {

? ? workInProgress = createFiber(...);

? }

? ...

? workInProgress.alternate = current;

? current.alternate = workInProgress;

? ...

? return workInProgress;

}

```

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf31a542dd0?w=986&h=890&f=jpeg&s=50391)

**alternate** fiber可以理解為一個fiber版本池,用于交替記錄組件更新(切分任務(wù)后變成多階段更新)過程中fiber的更新,因為在組件更新的各階段,更新前及更新過程中fiber狀態(tài)并不一致,在需要恢復(fù)時(如發(fā)生沖突),即可使用另一者直接回退至上一版本fiber。

Dan在[Beyond React 16](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html)演講中用了一個非常恰當(dāng)?shù)谋扔?,那就是Git 功能分支,你可以將 WIP 樹想象成從舊樹中 Fork 出來的功能分支,你在這新分支中添加或移除特性,即使是操作失誤也不會影響舊的分支。當(dāng)你這個分支經(jīng)過了測試和完善,就可以合并到舊分支,將其替換掉。

<!--### FiberRoot-->

<!--應(yīng)用的起點-->

### Update

- 用于記錄組件狀態(tài)的改變

- 存放于fiber的updateQueue里面

- 多個update同時存在

比如設(shè)置三個setState(),React是不會立即更新的,而是放到UpdateQueue中,再去更新

> ps: setState一直有人疑問為啥不是同步,將 setState() 視為請求而不是立即更新組件的命令。為了更好的感知性能,React 會延遲調(diào)用它,然后通過一次傳遞更新多個組件。React 并不會保證 state 的變更會立即生效。

```

export function createUpdate(

? expirationTime: ExpirationTime,

? suspenseConfig: null | SuspenseConfig,

): Update<*> {

? let update: Update<*> = {

? ? //任務(wù)過期事件

? ? //在創(chuàng)建每個更新的時候,需要設(shè)定過期時間,過期時間也就是優(yōu)先級。過期時間越長,就表示優(yōu)先級越低。

? ? expirationTime,

? ? // suspense的配置

? ? suspenseConfig,

? // export const UpdateState = 0; 表示更新State

? // export const ReplaceState = 1; 表示替換State

? // export const ForceUpdate = 2; 強制更新

? // export const CaptureUpdate = 3; 捕獲更新(發(fā)生異常錯誤的時候發(fā)生)

? // 指定更新的類型,值為以上幾種

? ? tag: UpdateState,

? ? // 更新內(nèi)容,比如`setState`接收的第一個參數(shù)

? ? payload: null,

? ? // 更新完成后的回調(diào),`setState`,`render`都有

? ? callback: null,

? ? // 指向下一個update

? ? // 單鏈表update queue通過 next串聯(lián)

? ? next: null,


? ? // 下一個side effect

? ? // 最新源碼被拋棄 next替換

? ? //nextEffect: null,

? };

? if (__DEV__) {

? ? update.priority = getCurrentPriorityLevel();

? }

? return update;

}

```

#### UpdateQueue

```

//創(chuàng)建更新隊列

export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {

? const queue: UpdateQueue<State> = {

? ? //應(yīng)用更新后的state

? ? baseState,

? ? //隊列中的第一個update

? ? firstUpdate: null,

? ? //隊列中的最后一個update

? ? lastUpdate: null,

? ? //隊列中第一個捕獲類型的update

? ? firstCapturedUpdate: null,

? ? //隊列中最后一個捕獲類型的update

? ? lastCapturedUpdate: null,

? ? //第一個side effect

? ? firstEffect: null,

? ? //最后一個side effect

? ? lastEffect: null,

? ? firstCapturedEffect: null,

? ? lastCapturedEffect: null,

? };

? return queue;

}

```

update中的payload:通常我們現(xiàn)在在調(diào)用setState傳入的是一個對象,但在使用fiber conciler時,必須傳入一個函數(shù),函數(shù)的返回值是要更新的state。react從很早的版本就開始支持這種寫法了,不過通常沒有人用。在之后的react版本中,可能會廢棄直接傳入對象的寫法。

```

setState({}, callback); // stack conciler

setState(() => { return {} }, callback); // fiber conciler

```

[ReactUpdateQueue源碼](https://github.com/facebook/react/blob/0f3838a01b0fda0ac5fd054c6be13166697a113c/packages/react-reconciler/src/ReactUpdateQueue.js)

#### Updater

每個組件都會有一個Updater對象,它的用處就是把組件元素更新和對應(yīng)的fiber關(guān)聯(lián)起來。監(jiān)聽組件元素的更新,并把對應(yīng)的更新放入該元素對應(yīng)的fiber的UpdateQueue里面,并且調(diào)用ScheduleWork方法,把最新的fiber讓scheduler去調(diào)度工作。

```

const classComponentUpdater = {

? isMounted,

? enqueueSetState(inst, payload, callback) {

? ? const fiber = getInstance(inst);

? ? const currentTime = requestCurrentTimeForUpdate();

? ? const suspenseConfig = requestCurrentSuspenseConfig();

? ? const expirationTime = computeExpirationForFiber(

? ? ? currentTime,

? ? ? fiber,

? ? ? suspenseConfig,

? ? );

? ? const update = createUpdate(expirationTime, suspenseConfig);

? ? update.payload = payload;

? ? if (callback !== undefined && callback !== null) {

? ? ? if (__DEV__) {

? ? ? ? warnOnInvalidCallback(callback, 'setState');

? ? ? }

? ? ? update.callback = callback;

? ? }

? ? enqueueUpdate(fiber, update);

? ? scheduleWork(fiber, expirationTime);

? },

? enqueueReplaceState(inst, payload, callback) {

? ? //一樣的代碼

? ? //...

? ? update.tag = ReplaceState;

? ? //...

? },

? enqueueForceUpdate(inst, callback) {

? ? //一樣的代碼

? ? //...

? ? update.tag = ForceUpdate;

? ? //...

? },

};

```

[ReactUpdateQueue=>classComponentUpdater](https://github.com/facebook/react/blob/9ac42dd074c42b66ecc0334b75200b1d2989f892/packages/react-reconciler/src/ReactFiberClassComponent.js#L182)

###? Effect list

**Side Effects**:我們可以將React中的一個組件視為一個使用state和props來計算UI的函數(shù)。每個其他活動,如改變DOM或調(diào)用生命周期方法,都應(yīng)該被認(rèn)為是side-effects,react文檔中是這樣描述的side-effects的:

>You’ve likely performed data fetching, subscriptions, or manually changing the DOM 的from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.

<!--可以看到大多數(shù)state和props更新將side-effects。由于應(yīng)用effects是一種work,fiber節(jié)點是一種方便的機(jī)制,可以跟蹤除更新之外的effects。每個fiber節(jié)點都可以具有與之相關(guān)的effects, 通過fiber節(jié)點中的effectTag字段表示。-->

React能夠非??焖俚馗?,并且為了實現(xiàn)高性能,它采用了一些有趣的技術(shù)。其中之一是構(gòu)建帶有side-effects的fiber節(jié)點的線性列表,其具有快速迭代的效果。迭代線性列表比樹快得多,并且沒有必要在沒有side effects的節(jié)點上花費時間。

每個fiber節(jié)點都可以具有與之相關(guān)的effects, 通過fiber節(jié)點中的effectTag字段表示。

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf369989012?w=654&h=578&f=jpeg&s=53973)

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf38c4cce67?w=635&h=351&f=jpeg&s=22245)

此列表的目標(biāo)是標(biāo)記具有DOM更新或與其關(guān)聯(lián)的其他effects的節(jié)點,此列表是WIP tree的子集,并使用nextEffect屬性,而不是current和workInProgress樹中使用的child屬性進(jìn)行鏈接。

## How it work

#### 核心目標(biāo)

- 把可中斷的工作拆分成多個小任務(wù)

- 為不同類型的更新分配任務(wù)優(yōu)先級

- 更新時能夠暫停,終止,復(fù)用渲染任務(wù)

### 更新過程概述

<!--- ReactDOM.render() 和 setState 的時候開始創(chuàng)建更新。-->

<!--- 將創(chuàng)建的更新加入任務(wù)隊列,等待調(diào)度。-->

<!--- 在 requestIdleCallback 空閑時執(zhí)行任務(wù)。-->

<!--- 從根節(jié)點開始遍歷 Fiber Node,并且構(gòu)建 WokeInProgress Tree。-->

<!--- 生成 EffectList。-->

<!--- 根據(jù) EffectList 更新 DOM。-->

我們先看看其Fiber的更新過程,然后再針對過程中的核心技術(shù)進(jìn)行展開。?

Reconciliation分為兩個階段:reconciliation 和 commit

#### reconciliation

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf3b859392c?w=1302&h=472&f=png&s=203638)

從圖中可以看到,可以把reconciler階段分為三部分,分別以紅線劃分。簡單的概括下三部分的工作:

1. 第一部分從? ReactDOM.render() 方法開始,把接收的React Element轉(zhuǎn)換為Fiber節(jié)點,并為其設(shè)置優(yōu)先級,記錄update等。這部分主要是一些數(shù)據(jù)方面的準(zhǔn)備工作。

2. 第二部分主要是三個函數(shù):scheduleWork、requestWork、performWork,即安排工作、申請工作、正式工作三部曲。React 16 新增的異步調(diào)用的功能則在這部分實現(xiàn)。

3. 第三部分是一個大循環(huán),遍歷所有的Fiber節(jié)點,通過Diff算法計算所有更新工作,產(chǎn)出 EffectList 給到commit階段使用。這部分的核心是 beginWork 函數(shù)。

#### commit階段

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf3d8667a1f?w=646&h=63&f=jpeg&s=8905)

這個階段主要做的工作拿到reconciliation階段產(chǎn)出的所有更新工作,提交這些工作并調(diào)用渲染模塊(react-dom)渲染UI。完成UI渲染之后,會調(diào)用剩余的生命周期函數(shù),所以異常處理也會在這部分進(jìn)行

### 分配優(yōu)先級

<!--![](https://ftp.bmp.ovh/imgs/2019/12/83174afa0d36c907.png)-->

<!--在三部曲中的 requestWork函數(shù)中,會判斷當(dāng)前任務(wù)是同步還是異步(暫時React的異步調(diào)用功能還在開發(fā)中,未開放使用,本文后續(xù)內(nèi)容是以同步任務(wù)為例),然后通過不同的方式調(diào)用任務(wù)。同步任務(wù)直接調(diào)用performWork函數(shù)立即執(zhí)行,-->

其上所列出的fiber結(jié)構(gòu)中有個expirationTime。

>expirationTime本質(zhì)上是fiber work執(zhí)行的優(yōu)先級。

```

// 源碼中的priorityLevel優(yōu)先級劃分

export const NoWork = 0;

// 僅僅比Never高一點 為了保證連續(xù)必須完整完成

export const Never = 1;

export const Idle = 2;

export const Sync = MAX_SIGNED_31_BIT_INT;//整型最大數(shù)值,是V8中針對32位系統(tǒng)所設(shè)置的最大值

export const Batched = Sync - 1;

```

<!--```-->

<!--export const ImmediatePriority: ReactPriorityLevel = 99;-->

<!--export const UserBlockingPriority: ReactPriorityLevel = 98;-->

<!--export const NormalPriority: ReactPriorityLevel = 97;-->

<!--export const LowPriority: ReactPriorityLevel = 96;-->

<!--export const IdlePriority: ReactPriorityLevel = 95;-->

<!--// NoPriority is the absence of priority. Also React-only.-->

<!--export const NoPriority: ReactPriorityLevel = 90;-->

<!--```-->

<!--```-->

<!--export function inferPriorityFromExpirationTime(-->

<!--? currentTime,-->

<!--? expirationTime,-->

<!--) {-->

<!--? if (expirationTime === Sync) {-->

<!--? ? return ImmediatePriority;-->

<!--? }-->

<!--? if (expirationTime === Never) {-->

<!--? ? return IdlePriority;-->

<!--? }-->

<!--? const msUntil =-->

<!--? ? expirationTimeToMs(expirationTime) - expirationTimeToMs(currentTime);-->

<!--? if (msUntil <= 0) {-->

<!--? ? return ImmediatePriority;-->

<!--? }-->

<!--? if (msUntil <= HIGH_PRIORITY_EXPIRATION + HIGH_PRIORITY_BATCH_SIZE) {-->

<!--? ? return UserBlockingPriority;-->

<!--? }-->

<!--? if (msUntil <= LOW_PRIORITY_EXPIRATION + LOW_PRIORITY_BATCH_SIZE) {-->

<!--? ? return NormalPriority;-->

<!--? }-->

<!--? // TODO: Handle LowPriority-->

<!--? // Assume anything lower has idle priority-->

<!--? return IdlePriority;-->

<!--}-->

<!--```-->

<!--通過把expirationTime和currentTime化為ms單位,并計算他們的差值,通過判斷差值落在哪個區(qū)間去判斷屬于哪個優(yōu)先級。-->

源碼中的[computeExpirationForFiber函數(shù)](https://github.com/facebook/react/blob/16.8.4/packages/react-reconciler/src/ReactFiberScheduler.js#L1595),該方法用于計算fiber更新任務(wù)的最晚執(zhí)行時間,進(jìn)行比較后,決定是否繼續(xù)做下一個任務(wù)。

```

//為fiber對象計算expirationTime

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {

? ...

? // 根據(jù)調(diào)度優(yōu)先級計算ExpirationTime

? ? const priorityLevel = getCurrentPriorityLevel();

? ? switch (priorityLevel) {

? ? ? case ImmediatePriority:

? ? ? ? expirationTime = Sync;

? ? ? ? break;

? ? ? ? //高優(yōu)先級 如由用戶輸入設(shè)計交互的任務(wù)

? ? ? case UserBlockingPriority:

? ? ? ? expirationTime = computeInteractiveExpiration(currentTime);

? ? ? ? break;

? ? ? ? // 正常的異步任務(wù)

? ? ? case NormalPriority:

? ? ? ? // This is a normal, concurrent update

? ? ? ? expirationTime = computeAsyncExpiration(currentTime);

? ? ? ? break;

? ? ? case LowPriority:

? ? ? case IdlePriority:

? ? ? ? expirationTime = Never;

? ? ? ? break;

? ? ? default:

? ? ? ? invariant(

? ? ? ? ? false,

? ? ? ? ? 'Unknown priority level. This error is likely caused by a bug in ' +

? ? ? ? ? ? 'React. Please file an issue.',

? ? ? ? );

? ? }

? ? ...

}

export const LOW_PRIORITY_EXPIRATION = 5000

export const LOW_PRIORITY_BATCH_SIZE = 250

export function computeAsyncExpiration(

? currentTime: ExpirationTime,

): ExpirationTime {

? return computeExpirationBucket(

? ? currentTime,

? ? LOW_PRIORITY_EXPIRATION,

? ? LOW_PRIORITY_BATCH_SIZE,

? )

}

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150

export const HIGH_PRIORITY_BATCH_SIZE = 100

export function computeInteractiveExpiration(currentTime: ExpirationTime) {

? return computeExpirationBucket(

? ? currentTime,

? ? HIGH_PRIORITY_EXPIRATION,

? ? HIGH_PRIORITY_BATCH_SIZE,

? )

}

function computeExpirationBucket(

? currentTime,

? expirationInMs,

? bucketSizeMs,

): ExpirationTime {

? return (

? ? MAGIC_NUMBER_OFFSET -

? ? ceiling(

? ? // 之前的算法

? ? //currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,

? ? ? MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,

? ? ? bucketSizeMs / UNIT_SIZE,

? ? )

? );

}

```

```

// 我們把公式整理一下:

// low

1073741821-ceiling(1073741821-currentTime+500,25) =>

1073741796-((1073742321-currentTime)/25 | 0)*25

// high

1073741821-ceiling(1073741821-currentTime+15,10)

```

簡單來說,最終結(jié)果是以25為單位向上增加的,比如說我們輸入102 - 126之間,最終得到的結(jié)果都是625,但是到了127得到的結(jié)果就是650了,這就是除以25取整的效果。

即計算出的React低優(yōu)先級update的expirationTime間隔是25ms, React讓兩個相近(25ms內(nèi))的update得到相同的expirationTime,目的就是讓這兩個update自動合并成一個Update,從而達(dá)到批量更新的目的。就像提到的doubleBuffer一樣,React為提高性能,考慮得非常全面!

expiration算法源碼

- [ReactFiberExpirationTime](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberExpirationTime.js)

- [SchedulerWithReactIntegration](https://github.com/facebook/react/blob/969f4b5bb8302afb3eb1656784130651047c3718/packages/react-reconciler/src/SchedulerWithReactIntegration.js)

推薦閱讀:[jokcy大神解析=》expirationTime計算](https://react.jokcy.me/book/update/expiration-time.html)

### 執(zhí)行優(yōu)先級

那么Fiber是如何做到異步實現(xiàn)不同優(yōu)先級任務(wù)的協(xié)調(diào)執(zhí)行的

這里要介紹介紹瀏覽器提供的兩個API:requestIdleCallback和requestAnimationFrame:

> requestIdleCallback:

在瀏覽器空閑時段內(nèi)調(diào)用的函數(shù)排隊。是開發(fā)人員可以在主事件循環(huán)上執(zhí)行后臺和低優(yōu)先級工作而不會影響延遲關(guān)鍵事件,如動畫和輸入響應(yīng)。

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf3e17d8ba3?w=1354&h=296&f=png&s=134124)

其在回調(diào)參數(shù)中IdleDeadline可以獲取到當(dāng)前幀剩余的時間。利用這個信息可以合理的安排當(dāng)前幀需要做的事情,如果時間足夠,那繼續(xù)做下一個任務(wù),如果時間不夠就歇一歇。

<!--還可以配置timeout參數(shù),當(dāng)任務(wù)超過多少時限未被執(zhí)行將被強制執(zhí)行,但有可能會造成失幀。-->

>? requestAnimationFrame:告訴瀏覽器你希望執(zhí)行一個動畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf40bb817f2?w=800&h=260&f=png&s=76689)

>合作式調(diào)度:這是一種’契約‘調(diào)度,要求我們的程序和瀏覽器緊密結(jié)合,互相信任。比如可以由瀏覽器給我們分配執(zhí)行時間片,我們要按照約定在這個時間內(nèi)執(zhí)行完畢,并將控制權(quán)還給瀏覽器。

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf43cb5a49c?w=831&h=594&f=jpeg&s=58798)

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf4220b0e19?w=595&h=358&f=jpeg&s=21862)

Fiber所做的就是需要分解渲染任務(wù),然后根據(jù)優(yōu)先級使用API調(diào)度,異步執(zhí)行指定任務(wù):?

- 低優(yōu)先級任務(wù)由requestIdleCallback處理,限制任務(wù)執(zhí)行時間,以切分任務(wù),同時避免任務(wù)長時間執(zhí)行,阻塞UI渲染而導(dǎo)致掉幀。?

- 高優(yōu)先級任務(wù),如動畫相關(guān)的由requestAnimationFrame處理;

并不是所有的瀏覽器都支持requestIdleCallback,但是React內(nèi)部實現(xiàn)了自己的polyfill,所以不必?fù)?dān)心瀏覽器兼容性問題。polyfill實現(xiàn)主要是通過rAF+postmessage實現(xiàn)的(最新版本去掉了rAF,有興趣的童鞋可以看看=》[SchedulerHostConfig](https://github.com/facebook/react/blob/a2e05b6c148b25590884e8911d4d4acfcb76a487/packages/scheduler/src/forks/SchedulerHostConfig.default.js)

## 生命周期

因為其在協(xié)調(diào)階段任務(wù)可被打斷的特點,任務(wù)在切片后運行完一段便將控制權(quán)交還到react負(fù)責(zé)任務(wù)調(diào)度的模塊,再根據(jù)任務(wù)的優(yōu)先級,繼續(xù)運行后面的任務(wù)。所以會導(dǎo)致某些組件渲染到一半便會打斷以運行其他緊急,優(yōu)先級更高的任務(wù),運行完卻不會繼續(xù)之前中斷的部分,而是重新開始,所以在協(xié)調(diào)的所有生命周期都會面臨這種被多次調(diào)用的情況。? ?

為了限制這種被多次重復(fù)調(diào)用,耗費性能的情況出現(xiàn),react官方一步步把處在協(xié)調(diào)階段的部分生命周期進(jìn)行移除。?

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf43e89053a?w=673&h=441&f=jpeg&s=31367)

廢棄:

- componentWillMount

- componentWillUpdate

- componentWillReceiveProps?

新增:

- [static getDerivedStateFromProps(props, state)](https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromprops)

- [getSnapshotBeforeUpdate(prevProps, prevState)](https://zh-hans.reactjs.org/docs/react-component.html#getsnapshotbeforeupdate)

- [componentDidcatch](https://zh-hans.reactjs.org/docs/react-component.html#componentdidcatch)

- [staic getderivedstatefromerror](https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromerror)

![newLifeCircle](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf4a482c926?w=800&h=438&f=png&s=128846)

#### 為什么新的生命周期用static

static 是ES6的寫法,當(dāng)我們定義一個函數(shù)為static時,就意味著無法通過this調(diào)用我們在類中定義的方法

通過static的寫法和函數(shù)參數(shù),可以感覺React在和我說:請只根據(jù)newProps來設(shè)定derived state,不要通過this這些東西來調(diào)用幫助方法,可能會越幫越亂。用專業(yè)術(shù)語說:getDerivedStateFromProps應(yīng)該是個純函數(shù),沒有副作用(side effect)。

#### getDerivedStateFromError和componentDidCatch之間的區(qū)別是什么?

簡而言之,因為所處**階段的不同**而功能不同。

getDerivedStateFromError是在reconciliation階段觸發(fā),所以getDerivedStateFromError進(jìn)行捕獲錯誤后進(jìn)行組件的狀態(tài)變更,不允許出現(xiàn)副作用。

```

static getDerivedStateFromError(error) {

? ? // 更新 state 使下一次渲染可以顯降級 UI

? ? return { hasError: true };

}

```

componentDidCatch因為在commit階段,因此允許執(zhí)行副作用。 它應(yīng)該用于記錄錯誤之類的情況:

```

componentDidCatch(error, info) {

? ? // "組件堆棧" 例子:

? ? //? in ComponentThatThrows (created by App)

? ? //? in ErrorBoundary (created by App)

? ? //? in div (created by App)

? ? //? in App

? ? logComponentStackToMyService(info.componentStack);

? }

```

<!--componentDidcatch和的差別-->

<!--https://stackoverflow.com/questions/52962851/whats-the-difference-between-getderivedstatefromerror-and-componentdidcatch-->

<!--https://zh-hans.reactjs.org/docs/error-boundaries.html#introducing-error-boundaries-->

生命周期相關(guān)資料點這里=》[生命周期](https://blog.csdn.net/Napoleonxxx/article/details/81120854)

## Suspense

Suspense的實現(xiàn)很詭異,也備受爭議。?

用Dan的原話講:你將會恨死它,然后你會愛上他。

**Suspense**功能想解決從react出生到現(xiàn)在都存在的「異步副作用」的問題,而且解決得非常的優(yōu)雅,使用的是「異步但是同步的寫法」.

Suspense暫時只是用于搭配lazy進(jìn)行代碼分割,在組件等待某事時“暫停”渲染的能力,并顯示加載的loading,但他的作用遠(yuǎn)遠(yuǎn)不止如此,當(dāng)下在concurrent mode實驗階段文檔下提供了一種suspense處理異步請求獲取數(shù)據(jù)的方法。

### 用法

```

// 懶加載組件切換時顯示過渡組件

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

// Show a spinner while the profile is loading

<Suspense fallback={<Spinner />}>

? <ProfilePage />

</Suspense>

```

<!--// This is not a Promise. It's a special object from our Suspense integration.-->

<!--// 這里fetchProfileData返回的不是promise,而是一個Suspense集成的特定對象-->

```

// 異步獲取數(shù)據(jù)

import { unstable_createResource } from 'react-cache'

const resource = unstable_createResource((id) => {

? return fetch(`/demo/${id}`)

})

function ProfilePage() {

? return (

? ? <Suspense fallback={<h1>Loading profile...</h1>}>

? ? ? <ProfileDetails />

? ? ? <Suspense fallback={<h1>Loading posts...</h1>}>

? ? ? ? <ProfileTimeline />

? ? ? </Suspense>

? ? </Suspense>

? );

}

function ProfileDetails() {

? // Try to read user info, although it might not have loaded yet

? const user = resource.user.read();

? return <h1>{user.name}</h1>;

}

function ProfileTimeline() {

? // Try to read posts, although they might not have loaded yet

? const posts = resource.posts.read();

? return (

? ? <ul>

? ? ? {posts.map(post => (

? ? ? ? <li key={post.id}>{post.text}</li>

? ? ? ))}

? ? </ul>

? );

}

```

- 在render函數(shù)中,我們可以寫入一個異步請求,請求數(shù)據(jù)

- react會從我們緩存中讀取這個緩存

- 如果有緩存了,直接進(jìn)行正常的render

- 如果沒有緩存,那么會拋出一個異常,這個異常是一個promise

- 當(dāng)這個promise完成后(請求數(shù)據(jù)完成),react會繼續(xù)回到原來的render中(實際上是重新執(zhí)行一遍render),把數(shù)據(jù)render出來

- 完全同步寫法,沒有任何異步callback之類的東西

如果你還沒有明白這是什么意思那我簡單的表述成下面這句話:

>調(diào)用render函數(shù)->發(fā)現(xiàn)有異步請求->懸停,等待異步請求結(jié)果->再渲染展示數(shù)據(jù)

看著是非常神奇的,用同步方法寫異步,而且沒有yield/async/await,簡直能把人看傻眼了。這么做的好處自然就是,我們的思維邏輯非常的簡單,清楚,沒有callback,沒有其他任何玩意,不能不說,看似優(yōu)雅了非常多而且牛逼。

官方文檔指出它還將提供官方的方法進(jìn)行數(shù)據(jù)獲取

### 原理

看一下react提供的unstable_createResource源碼

```

export function unstable_createResource(fetch, maybeHashInput) {

? const resource = {

? ? read(input) {

? ? ? ...

? ? ? const result = accessResult(resource, fetch, input, key);

? ? ? switch (result.status) {

? ? ? ? // 還未完成直接拋出自身promise

? ? ? ? case Pending: {

? ? ? ? ? const suspender = result.value;

? ? ? ? ? throw suspender;

? ? ? ? }

? ? ? ? case Resolved: {

? ? ? ? ? const value = result.value;

? ? ? ? ? return value;

? ? ? ? }

? ? ? ? case Rejected: {

? ? ? ? ? const error = result.value;

? ? ? ? ? throw error;

? ? ? ? }

? ? ? ? default:

? ? ? ? ? // Should be unreachable

? ? ? ? ? return (undefined: any);

? ? ? }

? ? },

? };

? return resource;

}

```

![](https://user-gold-cdn.xitu.io/2019/12/12/16ef9bf4de075d64?w=1262&h=840&f=jpeg&s=96470)

為此,React使用Promises。

組件可以在其render方法(或在組件的渲染過程中調(diào)用的任何東西,例如新的靜態(tài)getDerivedStateFromProps)中拋出Promise。

React捕獲了拋出的Promise,并在樹上尋找最接近的Suspense組件,Suspense其本身具有componentDidCatch,將promise當(dāng)成error捕獲,等待其執(zhí)行完成其更改狀態(tài)重新渲染子組件。?

Suspense組件將一個元素(fallback 作為其后備道具,無論子節(jié)點在何處或為什么掛起,都會在其子樹被掛起時進(jìn)行渲染。

#### 如何達(dá)成異常捕獲

1. reconciliation階段的 renderRoot 函數(shù),對應(yīng)異常處理方法是 throwException

2. commit階段的 commitRoot 函數(shù),對應(yīng)異常處理方法是 dispatch

#### reconciliation階段的異常捕獲?

react-reconciler中的[performConcurrentWorkOnRoot](https://github.com/facebook/react/blob/e039e690b5c45c458dd4026f3db16bac18ed0e47/packages/react-reconciler/src/ReactFiberWorkLoop.js#L642)

```

// This is the entry point for every concurrent task, i.e. anything that

// goes through Scheduler.

// 這里是每一個通過Scheduler的concurrent任務(wù)的入口

function performConcurrentWorkOnRoot(root, didTimeout) {

? ? ...

? ? do {

? ? ? ? try {

? ? ? ? ? ? //開始執(zhí)行Concurrent任務(wù)直到Scheduler要求我們讓步

? ? ? ? ? ? workLoopConcurrent();

? ? ? ? ? ? break;

? ? ? ? } catch (thrownValue) {

? ? ? ? ? ? handleError(root, thrownValue);

? ? ? ? }

? ? } while (true);

? ? ...

}

function handleError(root, thrownValue) {

? ? ...

? ? ? throwException(

? ? ? ? root,

? ? ? ? workInProgress.return,

? ? ? ? workInProgress,

? ? ? ? thrownValue,

? ? ? ? renderExpirationTime,

? ? ? );

? ? ? workInProgress = completeUnitOfWork(workInProgress);

? ...

}

```

[throwException](https://github.com/facebook/react/blob/f523b2e0d369e3f42938b56784f9ce1990838753/packages/react-reconciler/src/ReactFiberThrow.js#L178)

```

do {

? ? switch (workInProgress.tag) {

? ? ? ....

? ? ? case ClassComponent:

? ? ? ? // Capture and retry

? ? ? ? const errorInfo = value;

? ? ? ? const ctor = workInProgress.type;

? ? ? ? const instance = workInProgress.stateNode;

? ? ? ? if (

? ? ? ? ? (workInProgress.effectTag & DidCapture) === NoEffect &&

? ? ? ? ? (typeof ctor.getDerivedStateFromError === 'function' ||

? ? ? ? ? ? (instance !== null &&

? ? ? ? ? ? ? typeof instance.componentDidCatch === 'function' &&

? ? ? ? ? ? ? !isAlreadyFailedLegacyErrorBoundary(instance)))

? ? ? ? ) {

? ? ? ? ? workInProgress.effectTag |= ShouldCapture;

? ? ? ? ? workInProgress.expirationTime = renderExpirationTime;

? ? ? ? ? // Schedule the error boundary to re-render using updated state

? ? ? ? ? const update = createClassErrorUpdate(

? ? ? ? ? ? workInProgress,

? ? ? ? ? ? errorInfo,

? ? ? ? ? ? renderExpirationTime,

? ? ? ? ? );

? ? ? ? ? enqueueCapturedUpdate(workInProgress, update);

? ? ? ? ? return;

? ? ? ? }

? ? }

? ? ...

}


```

throwException函數(shù)分為兩部分

1、遍歷當(dāng)前異常節(jié)點的所有父節(jié)點,找到對應(yīng)的錯誤信息(錯誤名稱、調(diào)用棧等),這部分代碼在上面中沒有展示出來

2、第二部分是遍歷當(dāng)前異常節(jié)點的所有父節(jié)點,判斷各節(jié)點的類型,主要還是上面提到的兩種類型,這里重點講ClassComponent類型,判斷該節(jié)點是否是異常邊界組件(通過判斷是否存在componentDidCatch生命周期函數(shù)等),如果是找到異常邊界組件,則調(diào)用 createClassErrorUpdate函數(shù)新建update,并將此update放入此節(jié)點的異常更新隊列中,在后續(xù)更新中,會更新此隊列中的更新工作

#### commit階段

ReactFiberWorkLoop中的[finishConcurrentRender](https://github.com/facebook/react/blob/e039e690b5c45c458dd4026f3db16bac18ed0e47/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1709)=》

[commitRoot](https://github.com/facebook/react/blob/e039e690b5c45c458dd4026f3db16bac18ed0e47/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1709)=》

[commitRootImpl](https://github.com/facebook/react/blob/e039e690b5c45c458dd4026f3db16bac18ed0e47/packages/react-reconciler/src/ReactFiberWorkLoop.js#L1718)=》[captureCommitPhaseError](https://github.com/facebook/react/blob/e039e690b5c45c458dd4026f3db16bac18ed0e47/packages/react-reconciler/src/ReactFiberWorkLoop.js#L2280)

commit被分為幾個子階段,每個階段都try catch調(diào)用了一次captureCommitPhaseError

1. 突變(mutate)前階段:我們在突變前先讀出主樹的狀態(tài),getSnapshotBeforeUpdate在這里被調(diào)用

2. 突變階段:我們在這個階段更改主樹,完成WIP樹轉(zhuǎn)變?yōu)閏urrent樹

3. 樣式階段:調(diào)用從被更改后主樹讀取的effect

```

export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {

? if (sourceFiber.tag === HostRoot) {

? ? // Error was thrown at the root. There is no parent, so the root

? ? // itself should capture it.

? ? captureCommitPhaseErrorOnRoot(sourceFiber, sourceFiber, error);

? ? return;

? }

? let fiber = sourceFiber.return;

? while (fiber !== null) {

? ? if (fiber.tag === HostRoot) {

? ? ? captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);

? ? ? return;

? ? } else if (fiber.tag === ClassComponent) {

? ? ? const ctor = fiber.type;

? ? ? const instance = fiber.stateNode;

? ? ? if (

? ? ? ? typeof ctor.getDerivedStateFromError === 'function' ||

? ? ? ? (typeof instance.componentDidCatch === 'function' &&

? ? ? ? ? !isAlreadyFailedLegacyErrorBoundary(instance))

? ? ? ) {

? ? ? ? const errorInfo = createCapturedValue(error, sourceFiber);

? ? ? ? const update = createClassErrorUpdate(

? ? ? ? ? fiber,

? ? ? ? ? errorInfo,

? ? ? ? ? // TODO: This is always sync

? ? ? ? ? Sync,

? ? ? ? );

? ? ? ? enqueueUpdate(fiber, update);

? ? ? ? const root = markUpdateTimeFromFiberToRoot(fiber, Sync);

? ? ? ? if (root !== null) {

? ? ? ? ? ensureRootIsScheduled(root);

? ? ? ? ? schedulePendingInteractions(root, Sync);

? ? ? ? }

? ? ? ? return;

? ? ? }

? ? }

? ? fiber = fiber.return;

? }

}

```

captureCommitPhaseError函數(shù)做的事情和上部分的 throwException 類似,遍歷當(dāng)前異常節(jié)點的所有父節(jié)點,找到異常邊界組件(有componentDidCatch生命周期函數(shù)的組件),新建update,在update.callback中調(diào)用組件的componentDidCatch生命周期函數(shù)。

細(xì)心的小伙伴應(yīng)該注意到,throwException 和 captureCommitPhaseError在遍歷節(jié)點時,是從異常節(jié)點的父節(jié)點開始遍歷,所以異常捕獲一般由擁有componentDidCatch或getDerivedStateFromError的異常邊界組件進(jìn)行包裹,而其是無法捕獲并處理自身的報錯。

<!--2、commit階段的 commitRoot 函數(shù),對應(yīng)異常處理方法是 dispatch-->

## Hook相關(guān)

### Function Component和Class Component

Class component 劣勢

1.? 狀態(tài)邏輯難復(fù)用:在組件之間復(fù)用狀態(tài)邏輯很難,可能要用到 render props (渲染屬性)或者 HOC(高階組件),但無論是渲染屬性,還是高階組件,都會在原先的組件外包裹一層父容器(一般都是 div 元素),導(dǎo)致層級冗余 趨向復(fù)雜難以維護(hù):

2. 在生命周期函數(shù)中混雜不相干的邏輯(如:在 componentDidMount 中注冊事件以及其他的邏輯,在 componentWillUnmount 中卸載事件,這樣分散不集中的寫法,很容易寫出 bug ) 類組件中到處都是對狀態(tài)的訪問和處理,導(dǎo)致組件難以拆分成更小的組件

3. this 指向問題:父組件給子組件傳遞函數(shù)時,必須綁定 this

但是在16.8之前react的函數(shù)式組件十分羸弱,基本只能作用于純展示組件,主要因為缺少state和生命周期。

hooks優(yōu)勢

- 能優(yōu)化類組件的三大問題

- 能在無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯(自定義 Hooks )

- 能將組件中相互關(guān)聯(lián)的部分拆分成更小的函數(shù)(比如設(shè)置訂閱或請求數(shù)據(jù))

- 副作用的關(guān)注點分離:副作用指那些沒有發(fā)生在數(shù)據(jù)向視圖轉(zhuǎn)換過程中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設(shè)置定時器、記錄日志等。以往這些副作用都是寫在類組件生命周期函數(shù)中的。而 useEffect 在全部渲染完畢后才會執(zhí)行,useLayoutEffect 會在瀏覽器 layout 之后,painting 之前執(zhí)行。

### capture props和capture value特性

#### capture props

```

class ProfilePage extends React.Component {

? showMessage = () => {

? ? alert("Followed " + this.props.user);

? };

? handleClick = () => {

? ? setTimeout(this.showMessage, 3000);

? };

? render() {

? ? return <button onClick={this.handleClick}>Follow</button>;

? }

}

```

```

function ProfilePage(props) {

? const showMessage = () => {

? ? alert("Followed " + props.user);

? };

? const handleClick = () => {

? ? setTimeout(showMessage, 3000);

? };

? return <button onClick={handleClick}>Follow</button>;

}

```

這兩個組件都描述了同一個邏輯:點擊按鈕 3 秒后 alert 父級傳入的用戶名。

那么 React 文檔中描述的 props 不是不可變(Immutable) 數(shù)據(jù)嗎?為啥在運行時還會發(fā)生變化呢?

原因在于,雖然 props 不可變,是 this 在 Class Component 中是可變的,因此 this.props 的調(diào)用會導(dǎo)致每次都訪問最新的 props。

無可厚非,為了在生命周期和render重能拿到最新的版本react本身會實時更改this,這是this在class組件的本職。

這揭露了關(guān)于用戶界面的有趣觀察,如果我們說ui從概念上是一個當(dāng)前應(yīng)用狀態(tài)的函數(shù),事件處理就是render結(jié)果的一部分,我們的事件處理屬于擁有特定props或state的render。每次 Render 的內(nèi)容都會形成一個快照并保留下來,因此當(dāng)狀態(tài)變更而 Rerender 時,就形成了 N 個 Render 狀態(tài),而每個 Render 狀態(tài)都擁有自己固定不變的 Props 與 State。

然而在setTimeout的回調(diào)中獲取this.props會打斷這種的關(guān)聯(lián),失去了與某一特定render綁定,所以也失去了正確的props。

而 Function Component 不存在 this.props 的語法,因此 props 總是不可變的。

[測試地址](https://codesandbox.io/s/pjqnl16lm7)

#### hook中的capture value

```

function MessageThread() {

? const [message, setMessage] = useState("");

? const showMessage = () => {

? ? alert("You said: " + message);

? };

? const handleSendClick = () => {

? ? setTimeout(showMessage, 3000);

? };

? const handleMessageChange = e => {

? ? setMessage(e.target.value);

? };

? return (

? ? <>

? ? ? <input value={message} onChange={handleMessageChange} />

? ? ? <button onClick={handleSendClick}>Send</button>

? ? </>

? );

}

```

hook重同樣有capture value,每次渲染都有自己的 Props and State,如果要時刻獲取最新的值,規(guī)避 capture value 特性,可以用useRef

```

const lastest = useRef("");

const showMessage = () => {

? ? alert("You said: " + lastest.current);

};

const handleSendClick = () => {

? ? setTimeout(showMessage, 3000);

};

const handleMessageChange = e => {

? ? lastest.current = e.target.value;

};

```

[測試地址](https://codesandbox.io/s/93m5mz9w24)

### Hooks實現(xiàn)原理

在上面fiber結(jié)構(gòu)分析可以看出現(xiàn)在的Class component的state和props是記錄在fiber上的,在fiber更新后才會更新到component的this.state和props里面,而并不是class component自己調(diào)理的過程。這也給了實現(xiàn)hooks的方便,因為hooks是放在function component里面的,他沒有自己的this,但我們本身記錄state和props就不是放在class component this上面,而是在fiber上面,所以我們有能力記錄狀態(tài)之后,也有能力讓function? component更新過程當(dāng)中拿到更新之后的state。

### React 依賴于 Hook 的調(diào)用順序

日常調(diào)用三次

```

function Form() {

? const [hero, setHero] = useState('iron man');

? if(hero){

? ? const [surHero, setSurHero] = useState('Captain America');

? }

? const [nbHero, setNbHero] = useState('hulk');

? // ...

}

```

來看看我們的useState是怎么實現(xiàn)的

```

// useState 源碼中的鏈表實現(xiàn)

import React from 'react';

import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};

let workInProgressHook;

function useState(initState) {

? ? let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

? ? function setState(newState) {

? ? ? ? currentHook.memoizedState = newState;

? ? ? ? render();

? ? }

// 假如某個 useState 沒有執(zhí)行,會導(dǎo)致Next指針移動出錯,數(shù)據(jù)存取出錯

? ? if (workInProgressHook.next) {

? ? ? ? // 這里只有組件刷新的時候,才會進(jìn)入

? ? ? ? // 根據(jù)書寫順序來取對應(yīng)的值

? ? ? ? // console.log(workInProgressHook);

? ? ? ? workInProgressHook = workInProgressHook.next;

? ? } else {

? ? ? ? // 只有在組件初始化加載時,才會進(jìn)入

? ? ? ? // 根據(jù)書寫順序,存儲對應(yīng)的數(shù)據(jù)

? ? ? ? // 將 firstWorkInProgressHook 變成一個鏈表結(jié)構(gòu)

? ? ? ? workInProgressHook.next = currentHook;

? ? ? ? // 將 workInProgressHook 指向 {memoizedState: initState, next: null}

? ? ? ? workInProgressHook = currentHook;

? ? ? ? // console.log(firstWorkInProgressHook);

? ? }

? ? return [currentHook.memoizedState, setState];

}

function Counter() {

? ? // 每次組件重新渲染的時候,這里的 useState 都會重新執(zhí)行

? ? const [name, setName] = useState('計數(shù)器');

? ? const [number, setNumber] = useState(0);

? ? return (

? ? ? ? <>

? ? ? ? ? ? <p>{name}:{number}</p>

? ? ? ? ? ? <button onClick={() => setName('新計數(shù)器' + Date.now())}>新計數(shù)器</button>

? ? ? ? ? ? <button onClick={() => setNumber(number + 1)}>+</button>

? ? ? ? </>

? ? )

}

function render() {

? ? // 每次重新渲染的時候,都將 workInProgressHook 指向 firstWorkInProgressHook

? ? workInProgressHook = firstWorkInProgressHook;

? ? ReactDOM.render(<Counter/>, document.getElementById('root'));

}

render();

```

我們來還原一下這個過程

大家看完應(yīng)該了解,當(dāng)下設(shè)置currentHook其實是上個workInProgressHook通過next指針進(jìn)行綁定獲取的,所以如果在條件語句中打破了調(diào)用順序,將會導(dǎo)致next指針指向出現(xiàn)偏差,這個時候你傳進(jìn)去的setState是無法正確改變對應(yīng)的值,因為

<!--### 閉包-->

<!--咦,閉包跟hooks有關(guān)系嗎?-->

<!--簡單探討一下為啥-->

各種自定義封裝的hooks =》[react-use](https://github.com/streamich/react-use)

[為什么順序調(diào)用對 React Hooks 很重要?](https://overreacted.io/zh-hans/why-do-hooks-rely-on-call-order/)

<!--## Question:-->

<!--- 為什么使用深度優(yōu)先遍歷DFS?-->

<!--- -->

## THE END

> 第二次在掘金上發(fā)文,小陳也是react小菜??,希望能跟大家一起討論學(xué)習(xí),向高級前端架構(gòu)進(jìn)階!讓我們一起愛上fiber

## 參考:

[如何以及為什么React Fiber使用鏈表遍歷組件樹](

https://github.com/dawn-plex/translate/blob/master/articles/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-to-walk-the-components-tree.md)?

[React Fiber架構(gòu)](https://zhuanlan.zhihu.com/p/37095662)?

[React 源碼解析 - reactScheduler 異步任務(wù)調(diào)度](http://m.itdecent.cn/p/4a3a09925a28)?

[展望 React 17,回顧 React 往事 全面 深入](https://zhuanlan.zhihu.com/p/40160380)? ?

[這可能是最通俗的 React Fiber(時間分片) 打開方式](https://juejin.im/post/5dadc6045188255a270a0f85)=>調(diào)度策略?

[全面了解 React 新功能: Suspense 和 Hooks 生命周期](https://segmentfault.com/a/1190000017483690?utm_source=tag-newest)?

[詳談 React Fiber 架構(gòu)(1)](https://github.com/crazylxr/deep-in-react/blob/master/analysis/%E8%AF%A6%E8%B0%88%20React%20Fiber%20%E6%9E%B6%E6%9E%84(1).md)

<!--[componentdidcatch](https://github.com/HuJiaoHJ/blog/issues/12)-->

<!--dan talk about concurrent-->

<!--[https://twitter.com/dan_abramov/status/1120974978466439168]-->

<!-- The goal is to be responsive regardless of whether CPU or IO is lagging behind. So you want to *interleave* CPU and IO work. Let components render “in memory” while data for others is still streaming in, and show the final result when it’s ready. Not “fetch and mount”.-->

<!-- react-spring 演示了 React Concurrent-Mode 帶來的渲染提升-->

<!-- https://twitter.com/0xca0a/status/1199997552466288641-->

<!-- [你應(yīng)該知道的requestIdleCallback](https://segmentfault.com/a/1190000014457824)-->

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容