> 本文重點:介紹React重構(gòu)的起因和目的,理解Fiber tree單向鏈表結(jié)構(gòu)中各屬性含義吻商,梳理調(diào)度過程和核心實現(xiàn)手段掏颊,深入新的生命周期,hooks艾帐,suspense乌叶,異常捕獲等特性的用法和原理。
> 喜歡的就點個贊吧?柒爸,希望跟大家在枯燥的源碼中發(fā)掘?qū)W習(xí)的樂趣准浴,一起分享進步。
當(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)雅的異常處理,進行異常捕獲和備用ui渲染?
- 如何更好實現(xiàn)組件的復(fù)用和狀態(tài)管理?
<!--- setState為啥是異步的-->
<!--react性能優(yōu)化何去何從? -->
這究竟是人性的扭曲豹爹,還是道德的淪喪 /狗頭
Fiber能否給我們答案裆悄,又將帶給我們什么驚喜,卷起一波新的浪潮臂聋,歡迎收看《走進Fiber》
<!--是什么驅(qū)動react核心團隊對其核心算法進行重構(gòu)? -->
<!--他們又將有帶給我們什么驚喜光稼,卷起一波新的浪潮呢?? -->
那么孩等,簡而言之艾君,React Fiber是什么?
>**Fiber**是對React核心算法的重構(gòu)肄方,2年重構(gòu)的產(chǎn)物就是Fiber reconciler冰垄。
## react協(xié)調(diào)是什么

>協(xié)調(diào)是react中重要的一部分,其中包含了如何對新舊樹差異進行比較以達到僅更新差異的部分权她。?
現(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階段**:渲染器負責(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)
<!--線程:-->
<!--進程:-->
<!--為什么是單線程:-->
> 動畫是指由許多幀靜止的畫面,以一定的速度(如每秒16張)連續(xù)播放時崖瞭,肉眼因視覺殘象產(chǎn)生錯覺狂巢,而誤以為畫面活動的作品∈榫郏——維基百科
老一輩人常常把電影稱為“移動的畫”唧领,我們小時候看的手翻書就是快速翻動的一頁頁畫藻雌,其本質(zhì)上實現(xiàn)原理跟動畫是一樣的。
<!--<img src="https://ask.qcloudimg.com/http-save/yehe-2687933/l5f1eqnxee.gif" width="300">-->

幀:在動畫過程中斩个,每一幅靜止畫面即為一“幀”胯杭;?
幀率:是用于測量顯示幀數(shù)的量度,測量單位為“每秒顯示幀數(shù)”(Frame per Second受啥,F(xiàn)PS)或“赫茲”做个;?
幀時長:即每一幅靜止畫面的停留時間,單位一般是ms(毫秒)滚局;?
丟幀:在幀率固定的動畫中居暖,某一幀的時長遠高于平均幀時長,導(dǎo)致其后續(xù)數(shù)幀被擠壓而丟失的現(xiàn)象藤肢;
當(dāng)前大部分筆記本電腦和手機的常見幀率為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引擎線程

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

瀏覽器擁擠的主線程
React16 推出Fiber之前協(xié)調(diào)算法是Stack Reconciler甚疟,即遞歸遍歷所有的 Virtual DOM 節(jié)點執(zhí)行Diff算法仗岖,一旦開始便無法中斷,直到整顆虛擬dom樹構(gòu)建完成后才會釋放主線程览妖,因其JavaScript單線程的特點轧拄,若當(dāng)下組件具有復(fù)雜的嵌套和邏輯處理,diff便會堵塞UI進程讽膏,使動畫和交互等優(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)前正在進行的數(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í)行性能截圖:掉幀嚴重烹卒,普遍fps為1139.6ms

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

>究其原因是因為瀏覽器的主線程需要處理GUI描繪闷盔,時間器處理,事件處理旅急,JS執(zhí)行逢勾,遠程資源加載等,當(dāng)做某件事坠非,只有將它做完才能做下一件事敏沉。如果有足夠的時間,瀏覽器是會對我們的代碼進行編譯優(yōu)化(JIT)及進行熱代碼優(yōu)化炎码,一些DOM操作盟迟,內(nèi)部也會對reflow進行處理。reflow是一個性能黑洞潦闲,很可能讓頁面的大多數(shù)元素進行重新布局攒菠。
而作為一只有夢想的前端菜??,為用戶爸爸呈現(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階段所能拆分的最小工作單元
#### 針對其定義我們來進行拓展:
##### 虛擬堆棧幀:?
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)度的目標至關(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卡頓怔毛。

##### 可控的調(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)。

#### 具備扁平化的鏈表數(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來進行調(diào)度极景。
讓我們看一下fiber的結(jié)構(gòu)
```
type Fiber = {|
? // 標記不同的組件類型
? //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
? // 唯一標示。我們在寫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`
? // 共存的模式表示這個子樹是否默認是異步渲染的
? // Fiber被創(chuàng)建的時候他會繼承父Fiber
? // 其他的標識也可以在創(chuàng)建的時候被設(shè)置
? // 但是在創(chuàng)建之后不應(yīng)該再被修改棋返,特別是他的子Fiber創(chuàng)建之前
? //用來描述fiber是處于何種模式。用二進制位來表示(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)

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)分配暇务,增刪方便泼掠,輕量化,對異步友好
<!---->
###? 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就有一顆替代樹來準備刷新屏幕纱扭。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當(dāng)前樹儡遮。下次進來會把current狀態(tài)復(fù)制到WIP上乳蛾,進行交互復(fù)用,而不用每次更新的時候都創(chuàng)建一個新的對象,消耗性能屡久。這種同時緩存兩棵樹進行引用替換的技術(shù)被稱為**雙緩沖技術(shù)**忆首。
```
function createWorkInProgress(current, ...) {
? let workInProgress = current.alternate;
? if (workInProgress === null) {
? ? workInProgress = createFiber(...);
? }
? ...
? workInProgress.alternate = current;
? current.alternate = workInProgress;
? ...
? return workInProgress;
}
```

**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ù)谋扔鳎蔷褪荊it 功能分支版姑,你可以將 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)該被認為是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é)點是一種方便的機制肉康,可以跟蹤除更新之外的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字段表示。


此列表的目標是標記具有DOM更新或與其關(guān)聯(lián)的其他effects的節(jié)點塔粒,此列表是WIP tree的子集结借,并使用nextEffect屬性,而不是current和workInProgress樹中使用的child屬性進行鏈接卒茬。
## How it work
#### 核心目標
- 把可中斷的工作拆分成多個小任務(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ù)進行展開。?
Reconciliation分為兩個階段:reconciliation 和 commit
#### reconciliation

從圖中可以看到拉盾,可以把reconciler階段分為三部分,分別以紅線劃分雷激。簡單的概括下三部分的工作:
1. 第一部分從? ReactDOM.render() 方法開始屎暇,把接收的React Element轉(zhuǎn)換為Fiber節(jié)點,并為其設(shè)置優(yōu)先級驻粟,記錄update等根悼。這部分主要是一些數(shù)據(jù)方面的準備工作。
2. 第二部分主要是三個函數(shù):scheduleWork蜀撑、requestWork挤巡、performWork,即安排工作酷麦、申請工作矿卑、正式工作三部曲。React 16 新增的異步調(diào)用的功能則在這部分實現(xiàn)沃饶。
3. 第三部分是一個大循環(huán)母廷,遍歷所有的Fiber節(jié)點,通過Diff算法計算所有更新工作糊肤,產(chǎn)出 EffectList 給到commit階段使用琴昆。這部分的核心是 beginWork 函數(shù)。
#### commit階段

這個階段主要做的工作拿到reconciliation階段產(chǎn)出的所有更新工作馆揉,提交這些工作并調(diào)用渲染模塊(react-dom)渲染UI业舍。完成UI渲染之后,會調(diào)用剩余的生命周期函數(shù)升酣,所以異常處理也會在這部分進行
### 分配優(yōu)先級
<!---->
<!--在三部曲中的 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í)行時間,進行比較后祭阀,決定是否繼續(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,從而達到批量更新的目的板祝。就像提到的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)沸毁。

其在回調(diào)參數(shù)中IdleDeadline可以獲取到當(dāng)前幀剩余的時間。利用這個信息可以合理的安排當(dāng)前幀需要做的事情搂誉,如果時間足夠,那繼續(xù)做下一個任務(wù)侮腹,如果時間不夠就歇一歇。
<!--還可以配置timeout參數(shù)至非,當(dāng)任務(wù)超過多少時限未被執(zhí)行將被強制執(zhí)行,但有可能會造成失幀趣惠。-->
>? requestAnimationFrame:告訴瀏覽器你希望執(zhí)行一個動畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫

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


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缤沦,所以不必擔(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負責(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)階段的部分生命周期進行移除污筷。?

廢棄:
- 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)

#### 為什么新的生命周期用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進行捕獲錯誤后進行組件的狀態(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進行代碼分割载城,在組件等待某事時“暫兔呗欤”渲染的能力贵白,并顯示加載的loading劣领,但他的作用遠遠不止如此姐军,當(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會從我們緩存中讀取這個緩存
- 如果有緩存了村生,直接進行正常的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)雅了非常多而且牛逼。
官方文檔指出它還將提供官方的方法進行數(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;
}
```

為此淑掌,React使用Promises蒿讥。
組件可以在其render方法(或在組件的渲染過程中調(diào)用的任何東西,例如新的靜態(tài)getDerivedStateFromProps)中拋出Promise抛腕。
React捕獲了拋出的Promise芋绸,并在樹上尋找最接近的Suspense組件,Suspense其本身具有componentDidCatch担敌,將promise當(dāng)成error捕獲摔敛,等待其執(zhí)行完成其更改狀態(tài)重新渲染子組件。?
Suspense組件將一個元素(fallback 作為其后備道具全封,無論子節(jié)點在何處或為什么掛起马昙,都會在其子樹被掛起時進行渲染。
#### 如何達成異常捕獲
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ù)味榛。
細心的小伙伴應(yīng)該注意到,throwException 和 captureCommitPhaseError在遍歷節(jié)點時予跌,是從異常節(jié)點的父節(jié)點開始遍歷搏色,所以異常捕獲一般由擁有componentDidCatch或getDerivedStateFromError的異常邊界組件進行包裹,而其是無法捕獲并處理自身的報錯券册。
<!--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ù)雜難以維護:
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ù)書寫順序來取對應(yīng)的值
? ? ? ? // console.log(workInProgressHook);
? ? ? ? workInProgressHook = workInProgressHook.next;
? ? } else {
? ? ? ? // 只有在組件初始化加載時烹玉,才會進入
? ? ? ? // 根據(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指針進行綁定獲取的掂榔,所以如果在條件語句中打破了調(diào)用順序继效,將會導(dǎo)致next指針指向出現(xià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)進階帜乞!讓我們一起愛上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://www.reibang.com/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)-->