本文重點:介紹React重構的原由和目的,理解Fiber tree單向鏈表結構中各屬性含義,梳理調度過程和核心實現手段,深刻新的生命週期,hooks,suspense,異常捕獲等特性的用法和原理。css
喜歡的就點個贊吧️,但願跟你們在枯燥的源碼中發掘學習的樂趣,一塊兒分享進步。html
當react剛推出的時候,最具革命性的特性就是虛擬dom,由於這大大下降了應用開發的難度,相比較以往告訴瀏覽器我須要怎麼更新個人ui,如今咱們只須要告訴react我應用ui的下個狀態是怎麼樣的,react會幫咱們自動處理二者之間的全部事宜。前端
這讓咱們能夠從屬性操做、事件處理和手動 DOM 更新這些在構建應用程序時必要的操做中解放出來。宿主樹的概念讓這個優秀的框架有無限的可能性,react native即是其在原生移動應用中偉大的實現。react
但在享受溫馨開發體驗的同時,有一些疑問一直縈繞在咱們腦海中:git
這到底是人性的扭曲,仍是道德的淪喪 /狗頭github
Fiber可否給咱們答案,又將帶給咱們什麼驚喜,捲起一波新的浪潮,歡迎收看《走進Fiber》ajax
那麼,簡而言之,React Fiber是什麼?算法
Fiber是對React核心算法的重構,2年重構的產物就是Fiber reconciler。segmentfault
協調是react中重要的一部分,其中包含了如何對新舊樹差別進行比較以達到僅更新差別的部分。promise
如今的react通過重構後Reconciliation和Rendering被分爲兩個不一樣的階段。
動畫是指由許多幀靜止的畫面,以必定的速度(如每秒16張)連續播放時,肉眼因視覺殘象產生錯覺,而誤覺得畫面活動的做品。——維基百科
老一輩人經常把電影稱爲「移動的畫」,咱們小時候看的手翻書就是快速翻動的一頁頁畫,其本質上實現原理跟動畫是同樣的。
幀:在動畫過程當中,每一幅靜止畫面即爲一「幀」;
幀率:是用於測量顯示幀數的量度,測量單位爲「每秒顯示幀數」(Frame per Second,FPS)或「赫茲」;
幀時長:即每一幅靜止畫面的停留時間,單位通常是ms(毫秒);
丟幀:在幀率固定的動畫中,某一幀的時長遠高於平均幀時長,致使其後續數幀被擠壓而丟失的現象;
當前大部分筆記本電腦和手機的常見幀率爲60hz,即一秒顯示60幀的畫面,一幀停留的時間爲16.7ms(1000/60≈16.7),這就留給了開發者和UI系統大約16.67ms來完成生成一張靜態圖片(幀)所須要的全部工做。若是在這分派的16.67ms以內沒有可以完成這些工做,就會引起‘丟幀’的後果,使界面表現的不夠流暢。
瀏覽器中的GUI渲染線程和JS引擎線程
在瀏覽器中GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
React16 推出Fiber以前協調算法是Stack Reconciler,即遞歸遍歷全部的 Virtual DOM 節點執行Diff算法,一旦開始便沒法中斷,直到整顆虛擬dom樹構建完成後纔會釋放主線程,因其JavaScript單線程的特色,若當下組件具備複雜的嵌套和邏輯處理,diff便會堵塞UI進程,使動畫和交互等優先級相對較高的任務沒法當即獲得處理,形成頁面卡頓掉幀,影響用戶體驗。
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.
一旦將每一個堆棧幀做爲堆上的對象,您就能夠作一些聰明的事情,例如在未來的更新中重用它並暫停於事件循環,而不會丟失任何當前正在進行的數據。
咱們來作一個實驗
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個節點,共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執行性能截圖:掉幀嚴重,廣泛fps爲1139.6ms
b執行性能截圖: fps處於15ms~19ms
究其緣由是由於瀏覽器的主線程須要處理GUI描繪,時間器處理,事件處理,JS執行,遠程資源加載等,當作某件事,只有將它作完才能作下一件事。若是有足夠的時間,瀏覽器是會對咱們的代碼進行編譯優化(JIT)及進行熱代碼優化,一些DOM操做,內部也會對reflow進行處理。reflow是一個性能黑洞,極可能讓頁面的大多數元素進行從新佈局。
而做爲一隻有夢想的前端菜🐤,爲用戶爸爸呈現最好的交互體驗是咱們責無旁貸的責任,把困難扛在肩上,讓咱們see see react是如何解決以上的問題。
那麼咱們先看看做爲看看解決方案的Fiber是什麼,而後在分析爲何它能解決以上問題。
Andrew Clark的React Fiber體系文檔很好地解釋了Fiber實現背後的想法,我在這裏引用一下:
Fiber是堆棧的從新實現,專門用於React組件。 您能夠將單個Fiber視爲虛擬堆棧框架。 從新實現堆棧的優勢是,您能夠將堆棧幀保留在內存中,並根據須要(以及在任什麼時候候)執行它們。 這對於實現調度的目標相當重要。
JavaScript原生的執行模型:經過調用棧來管理函數執行狀態。
其中每一個棧幀表示一個工做單元(a unit of work),存儲了函數調用的返回指針、當前函數、調用參數、局部變量等信息。 由於JavaScript的執行棧是由引擎管理的,執行棧一旦開始,就會一直執行,直到執行棧清空。沒法按需停止。
react以往的渲染就是使用原生執行棧來管理組件樹的遞歸渲染,當其層次較深component不斷遞歸子節點,沒法被打斷就會致使主線程堵塞ui卡頓。
因此理想情況下reconciliation的過程應該是像下圖所示同樣,將繁重的任務劃分紅一個個小的工做單元,作完後可以「喘口氣兒」。咱們須要一種增量渲染的調度,Fiber就是從新實現一個堆棧幀的調度,這個堆棧幀能夠按照本身的調度算法執行他們。另外因爲這些堆棧是可將可中斷的任務拆分紅多個子任務,經過按照優先級來自由調度子任務,分段更新,從而將以前的同步渲染改成異步渲染。
它的特性就是時間分片(time slicing)和暫停(supense)。
fiber是一個js對象,fiber的建立是經過React元素來建立的,在整個React構建的虛擬DOM樹中,每個元素都對應有一個fiber,從而構建了一棵fiber樹,每一個fiber不只僅包含每一個元素的信息,還包含更多的信息,以方便Scheduler來進行調度。
讓咱們看一下fiber的結構
type Fiber = {|
// 標記不一樣的組件類型
//export const FunctionComponent = 0;
//export const ClassComponent = 1;
//export const HostRoot = 3; 能夠理解爲這個fiber是fiber樹的根節點,根節點能夠嵌套在子樹中
//export const Fragment = 7;
//export const SuspenseComponent = 13;
//export const MemoComponent = 14;
//export const LazyComponent = 16;
tag: WorkTag,
// ReactElement裏面的key
// 惟一標示。咱們在寫React的時候若是出現列表式的時候,須要制定key,這key就是對應元素的key。
key: null | string,
// ReactElement.type,也就是咱們調用`createElement`的第一個參數
elementType: any,
// The resolved function/class/ associated with this fiber.
// 異步組件resolved以後返回的內容,通常是`function`或者`class`
type: any,
// The local state associated with this fiber.
// 跟當前Fiber相關本地狀態(好比瀏覽器環境就是DOM節點)
// 當前組件實例的引用
stateNode: any,
// 指向他在Fiber節點樹中的`parent`,用來在處理完這個節點以後向上返回
return: Fiber | null,
// 單鏈表樹結構
// 指向本身的第一個子節點
child: Fiber | null,
// 指向本身的兄弟結構
// 兄弟節點的return指向同一個父節點
sibling: Fiber | null,
index: number,
// ref屬性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 新的變更帶來的新的props
pendingProps: any,
// 上一次渲染完成以後的props
memoizedProps: any,
// 該Fiber對應的組件產生的Update會存放在這個隊列裏面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的時候的state
// 用來存放某個組件內全部的 Hook 狀態
memoizedState: any,
// 一個列表,存放這個Fiber依賴的context
firstContextDependency: ContextDependency<mixed> | null,
// 用來描述當前Fiber和他子樹的`Bitfield`
// 共存的模式表示這個子樹是否默認是異步渲染的
// Fiber被建立的時候他會繼承父Fiber
// 其餘的標識也能夠在建立的時候被設置
// 可是在建立以後不該該再被修改,特別是他的子Fiber建立以前
//用來描述fiber是處於何種模式。用二進制位來表示(bitfield),後面經過與來看二者是否相同//這個字段實際上是一個數字.實現定義了一下四種//NoContext: 0b000->0//AsyncMode: 0b001->1//StrictMode: 0b010->2//ProfileMode: 0b100->4
mode: TypeOfMode,
// Effect
// 用來記錄Side Effect具體的執行的工做的類型:好比Placement,Update等等
effectTag: SideEffectTag,
// 單鏈表用來快速查找下一個side effect
nextEffect: Fiber | null,
// 子樹中第一個side effect
firstEffect: Fiber | null,
// 子樹中最後一個side effect
lastEffect: Fiber | null,
// 表明任務在將來的哪一個時間點應該被完成
// 不包括他的子樹產生的任務
// 經過這個參數也能夠知道是否還有等待暫停的變動、沒有完成變動。
// 這個參數通常是UpdateQueue中最長過時時間的Update相同,若是有Update的話。
expirationTime: ExpirationTime,
// 快速肯定子樹中是否有不在等待的變化
childExpirationTime: ExpirationTime,
//當前fiber對應的工做中的Fiber。
// 在Fiber樹更新的過程當中,每一個Fiber都會有一個跟其對應的Fiber
// 咱們稱他爲 current <==> workInProgress
// 在渲染完成以後他們會交換位置
alternate: Fiber | null,
...
|};
複製代碼
React16特別青睞於鏈表結構,鏈表在內存裏不是連續的,動態分配,增刪方便,輕量化,對異步友好
current樹:React 在 render 第一次渲染時,會經過 React.createElement 建立一顆 Element 樹,能夠稱之爲 Virtual DOM Tree,因爲要記錄上下文信息,加入了 Fiber,每個 Element 會對應一個 Fiber Node,將 Fiber Node 連接起來的結構成爲 Fiber Tree。它反映了用於渲染 UI 和映射應用狀態。這棵樹一般被稱爲 current 樹(當前樹,記錄當前頁面的狀態)。
workInProgress樹:當React通過current當前樹時,對於每個先存在的fiber節點,它都會建立一個替代(alternate)節點,這些節點組成了workInProgress樹。這個節點是使用render方法返回的React元素的數據建立的。一旦更新處理完以及全部相關工做完成,React就有一顆替代樹來準備刷新屏幕。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當前樹。下次進來會把current狀態複製到WIP上,進行交互複用,而不用每次更新的時候都建立一個新的對象,消耗性能。這種同時緩存兩棵樹進行引用替換的技術被稱爲雙緩衝技術。
function createWorkInProgress(current, ...) {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(...);
}
...
workInProgress.alternate = current;
current.alternate = workInProgress;
...
return workInProgress;
}
複製代碼
Dan在Beyond React 16演講中用了一個很是恰當的比喻,那就是Git 功能分支,你能夠將 WIP 樹想象成從舊樹中 Fork 出來的功能分支,你在這新分支中添加或移除特性,即便是操做失誤也不會影響舊的分支。當你這個分支通過了測試和完善,就能夠合併到舊分支,將其替換掉。
好比設置三個setState(),React是不會當即更新的,而是放到UpdateQueue中,再去更新
ps: setState一直有人疑問爲啥不是同步,將 setState() 視爲請求而不是當即更新組件的命令。爲了更好的感知性能,React 會延遲調用它,而後經過一次傳遞更新多個組件。React 並不會保證 state 的變動會當即生效。
export function createUpdate(
expirationTime: ExpirationTime,
suspenseConfig: null | SuspenseConfig,
): Update<*> {
let update: Update<*> = {
//任務過時事件
//在建立每一個更新的時候,須要設定過時時間,過時時間也就是優先級。過時時間越長,就表示優先級越低。
expirationTime,
// suspense的配置
suspenseConfig,
// export const UpdateState = 0; 表示更新State
// export const ReplaceState = 1; 表示替換State
// export const ForceUpdate = 2; 強制更新
// export const CaptureUpdate = 3; 捕獲更新(發生異常錯誤的時候發生)
// 指定更新的類型,值爲以上幾種
tag: UpdateState,
// 更新內容,好比`setState`接收的第一個參數
payload: null,
// 更新完成後的回調,`setState`,`render`都有
callback: null,
// 指向下一個update
// 單鏈表update queue經過 next串聯
next: null,
// 下一個side effect
// 最新源碼被拋棄 next替換
//nextEffect: null,
};
if (__DEV__) {
update.priority = getCurrentPriorityLevel();
}
return update;
}
複製代碼
//建立更新隊列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
//應用更新後的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:一般咱們如今在調用setState傳入的是一個對象,但在使用fiber conciler時,必須傳入一個函數,函數的返回值是要更新的state。react從很早的版本就開始支持這種寫法了,不過一般沒有人用。在以後的react版本中,可能會廢棄直接傳入對象的寫法。
setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
複製代碼
每一個組件都會有一個Updater對象,它的用處就是把組件元素更新和對應的fiber關聯起來。監聽組件元素的更新,並把對應的更新放入該元素對應的fiber的UpdateQueue裏面,而且調用ScheduleWork方法,把最新的fiber讓scheduler去調度工做。
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
Side Effects:咱們能夠將React中的一個組件視爲一個使用state和props來計算UI的函數。每一個其餘活動,如改變DOM或調用生命週期方法,都應該被認爲是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.
React可以很是快速地更新,而且爲了實現高性能,它採用了一些有趣的技術。其中之一是構建帶有side-effects的fiber節點的線性列表,其具備快速迭代的效果。迭代線性列表比樹快得多,而且沒有必要在沒有side effects的節點上花費時間。
每一個fiber節點均可以具備與之相關的effects, 經過fiber節點中的effectTag字段表示。
此列表的目標是標記具備DOM更新或與其關聯的其餘effects的節點,此列表是WIP tree的子集,並使用nextEffect屬性,而不是current和workInProgress樹中使用的child屬性進行連接。
咱們先看看其Fiber的更新過程,而後再針對過程當中的核心技術進行展開。
Reconciliation分爲兩個階段:reconciliation 和 commit
這個階段主要作的工做拿到reconciliation階段產出的全部更新工做,提交這些工做並調用渲染模塊(react-dom)渲染UI。完成UI渲染以後,會調用剩餘的生命週期函數,因此異常處理也會在這部分進行
其上所列出的fiber結構中有個expirationTime。
expirationTime本質上是fiber work執行的優先級。
// 源碼中的priorityLevel優先級劃分
export const NoWork = 0;
// 僅僅比Never高一點 爲了保證連續必須完整完成
export const Never = 1;
export const Idle = 2;
export const Sync = MAX_SIGNED_31_BIT_INT;//整型最大數值,是V8中針對32位系統所設置的最大值
export const Batched = Sync - 1;
複製代碼
源碼中的computeExpirationForFiber函數,該方法用於計算fiber更新任務的最晚執行時間,進行比較後,決定是否繼續作下一個任務。
//爲fiber對象計算expirationTime
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
...
// 根據調度優先級計算ExpirationTime
const priorityLevel = getCurrentPriorityLevel();
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
//高優先級 如由用戶輸入設計交互的任務
case UserBlockingPriority:
expirationTime = computeInteractiveExpiration(currentTime);
break;
// 正常的異步任務
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)
複製代碼
簡單來講,最終結果是以25爲單位向上增長的,好比說咱們輸入102 - 126之間,最終獲得的結果都是625,可是到了127獲得的結果就是650了,這就是除以25取整的效果。
即計算出的React低優先級update的expirationTime間隔是25ms, React讓兩個相近(25ms內)的update獲得相同的expirationTime,目的就是讓這兩個update自動合併成一個Update,從而達到批量更新的目的。就像提到的doubleBuffer同樣,React爲提升性能,考慮得很是全面!
expiration算法源碼
推薦閱讀:jokcy大神解析=》expirationTime計算
那麼Fiber是如何作到異步實現不一樣優先級任務的協調執行的
這裏要介紹介紹瀏覽器提供的兩個API:requestIdleCallback和requestAnimationFrame:
requestIdleCallback: 在瀏覽器空閒時段內調用的函數排隊。是開發人員能夠在主事件循環上執行後臺和低優先級工做而不會影響延遲關鍵事件,如動畫和輸入響應。
其在回調參數中IdleDeadline能夠獲取到當前幀剩餘的時間。利用這個信息能夠合理的安排當前幀須要作的事情,若是時間足夠,那繼續作下一個任務,若是時間不夠就歇一歇。
requestAnimationFrame:告訴瀏覽器你但願執行一個動畫,而且要求瀏覽器在下次重繪以前調用指定的回調函數更新動畫
合做式調度:這是一種’契約‘調度,要求咱們的程序和瀏覽器緊密結合,互相信任。好比能夠由瀏覽器給咱們分配執行時間片,咱們要按照約定在這個時間內執行完畢,並將控制權還給瀏覽器。
Fiber所作的就是須要分解渲染任務,而後根據優先級使用API調度,異步執行指定任務:
並非全部的瀏覽器都支持requestIdleCallback,可是React內部實現了本身的polyfill,因此沒必要擔憂瀏覽器兼容性問題。polyfill實現主要是經過rAF+postmessage實現的(最新版本去掉了rAF,有興趣的童鞋能夠看看=》SchedulerHostConfig
由於其在協調階段任務可被打斷的特色,任務在切片後運行完一段便將控制權交還到react負責任務調度的模塊,再根據任務的優先級,繼續運行後面的任務。因此會致使某些組件渲染到一半便會打斷以運行其餘緊急,優先級更高的任務,運行完卻不會繼續以前中斷的部分,而是從新開始,因此在協調的全部生命週期都會面臨這種被屢次調用的狀況。
爲了限制這種被屢次重複調用,耗費性能的狀況出現,react官方一步步把處在協調階段的部分生命週期進行移除。
廢棄:
新增:
static 是ES6的寫法,當咱們定義一個函數爲static時,就意味着沒法經過this調用咱們在類中定義的方法
經過static的寫法和函數參數,能夠感受React在和我說:請只根據newProps來設定derived state,不要經過this這些東西來調用幫助方法,可能會越幫越亂。用專業術語說:getDerivedStateFromProps應該是個純函數,沒有反作用(side effect)。
簡而言之,由於所處階段的不一樣而功能不一樣。
getDerivedStateFromError是在reconciliation階段觸發,因此getDerivedStateFromError進行捕獲錯誤後進行組件的狀態變動,不容許出現反作用。
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能夠顯降級 UI
return { hasError: true };
}
複製代碼
componentDidCatch由於在commit階段,所以容許執行反作用。 它應該用於記錄錯誤之類的狀況:
componentDidCatch(error, info) {
// "組件堆棧" 例子:
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logComponentStackToMyService(info.componentStack);
}
複製代碼
生命週期相關資料點這裏=》生命週期
Suspense的實現很詭異,也備受爭議。
用Dan的原話講:你將會恨死它,而後你會愛上他。
Suspense功能想解決從react出生到如今都存在的「異步反作用」的問題,並且解決得很是的優雅,使用的是「異步可是同步的寫法」.
Suspense暫時只是用於搭配lazy進行代碼分割,在組件等待某事時「暫停」渲染的能力,並顯示加載的loading,但他的做用遠遠不止如此,當下在concurrent mode實驗階段文檔下提供了一種suspense處理異步請求獲取數據的方法。
// 懶加載組件切換時顯示過渡組件
const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded
// Show a spinner while the profile is loading
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>
複製代碼
// 異步獲取數據
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函數->發現有異步請求->懸停,等待異步請求結果->再渲染展現數據
看着是很是神奇的,用同步方法寫異步,並且沒有yield/async/await,簡直能把人看傻眼了。這麼作的好處天然就是,咱們的思惟邏輯很是的簡單,清楚,沒有callback,沒有其餘任何玩意,不能不說,看似優雅了很是多並且牛逼。
官方文檔指出它還將提供官方的方法進行數據獲取
看一下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方法(或在組件的渲染過程當中調用的任何東西,例如新的靜態getDerivedStateFromProps)中拋出Promise。 React捕獲了拋出的Promise,並在樹上尋找最接近的Suspense組件,Suspense其自己具備componentDidCatch,將promise當成error捕獲,等待其執行完成其更改狀態從新渲染子組件。
Suspense組件將一個元素(fallback 做爲其後備道具,不管子節點在何處或爲何掛起,都會在其子樹被掛起時進行渲染。
react-reconciler中的performConcurrentWorkOnRoot
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
// 這裏是每個經過Scheduler的concurrent任務的入口
function performConcurrentWorkOnRoot(root, didTimeout) {
...
do {
try {
//開始執行Concurrent任務直到Scheduler要求咱們讓步
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
...
}
function handleError(root, thrownValue) {
...
throwException(
root,
workInProgress.return,
workInProgress,
thrownValue,
renderExpirationTime,
);
workInProgress = completeUnitOfWork(workInProgress);
...
}
複製代碼
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函數分爲兩部分 一、遍歷當前異常節點的全部父節點,找到對應的錯誤信息(錯誤名稱、調用棧等),這部分代碼在上面中沒有展現出來
二、第二部分是遍歷當前異常節點的全部父節點,判斷各節點的類型,主要仍是上面提到的兩種類型,這裏重點講ClassComponent類型,判斷該節點是不是異常邊界組件(經過判斷是否存在componentDidCatch生命週期函數等),若是是找到異常邊界組件,則調用 createClassErrorUpdate函數新建update,並將此update放入此節點的異常更新隊列中,在後續更新中,會更新此隊列中的更新工做
ReactFiberWorkLoop中的finishConcurrentRender=》 commitRoot=》 commitRootImpl=》captureCommitPhaseError
commit被分爲幾個子階段,每一個階段都try catch調用了一次captureCommitPhaseError
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函數作的事情和上部分的 throwException 相似,遍歷當前異常節點的全部父節點,找到異常邊界組件(有componentDidCatch生命週期函數的組件),新建update,在update.callback中調用組件的componentDidCatch生命週期函數。
細心的小夥伴應該注意到,throwException 和 captureCommitPhaseError在遍歷節點時,是從異常節點的父節點開始遍歷,因此異常捕獲通常由擁有componentDidCatch或getDerivedStateFromError的異常邊界組件進行包裹,而其是沒法捕獲並處理自身的報錯。
Class component 劣勢
可是在16.8以前react的函數式組件十分羸弱,基本只能做用於純展現組件,主要由於缺乏state和生命週期。
hooks優點
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) 數據嗎?爲啥在運行時還會發生變化呢?
緣由在於,雖然 props 不可變,是 this 在 Class Component 中是可變的,所以 this.props 的調用會致使每次都訪問最新的 props。
無可厚非,爲了在生命週期和render重能拿到最新的版本react自己會實時更改this,這是this在class組件的本職。
這揭露了關於用戶界面的有趣觀察,若是咱們說ui從概念上是一個當前應用狀態的函數,事件處理就是render結果的一部分,咱們的事件處理屬於擁有特定props或state的render。每次 Render 的內容都會造成一個快照並保留下來,所以當狀態變動而 Rerender 時,就造成了 N 個 Render 狀態,而每一個 Render 狀態都擁有本身固定不變的 Props 與 State。
然而在setTimeout的回調中獲取this.props會打斷這種的關聯,失去了與某一特定render綁定,因此也失去了正確的props。
而 Function Component 不存在 this.props 的語法,所以 props 老是不可變的。
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,若是要時刻獲取最新的值,規避 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;
};
複製代碼
在上面fiber結構分析能夠看出如今的Class component的state和props是記錄在fiber上的,在fiber更新後纔會更新到component的this.state和props裏面,而並非class component本身調理的過程。這也給了實現hooks的方便,由於hooks是放在function component裏面的,他沒有本身的this,但咱們自己記錄state和props就不是放在class component this上面,而是在fiber上面,因此咱們有能力記錄狀態以後,也有能力讓function component更新過程中拿到更新以後的state。
平常調用三次
function Form() {
const [hero, setHero] = useState('iron man');
if(hero){
const [surHero, setSurHero] = useState('Captain America');
}
const [nbHero, setNbHero] = useState('hulk');
// ...
}
複製代碼
來看看咱們的useState是怎麼實現的
// useState 源碼中的鏈表實現
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 沒有執行,會致使Next指針移動出錯,數據存取出錯
if (workInProgressHook.next) {
// 這裏只有組件刷新的時候,纔會進入
// 根據書寫順序來取對應的值
// console.log(workInProgressHook);
workInProgressHook = workInProgressHook.next;
} else {
// 只有在組件初始化加載時,纔會進入
// 根據書寫順序,存儲對應的數據
// 將 firstWorkInProgressHook 變成一個鏈表結構
workInProgressHook.next = currentHook;
// 將 workInProgressHook 指向 {memoizedState: initState, next: null}
workInProgressHook = currentHook;
// console.log(firstWorkInProgressHook);
}
return [currentHook.memoizedState, setState];
}
function Counter() {
// 每次組件從新渲染的時候,這裏的 useState 都會從新執行
const [name, setName] = useState('計數器');
const [number, setNumber] = useState(0);
return (
<>
<p>{name}:{number}</p>
<button onClick={() => setName('新計數器' + Date.now())}>新計數器</button>
<button onClick={() => setNumber(number + 1)}>+</button>
</>
)
}
function render() {
// 每次從新渲染的時候,都將 workInProgressHook 指向 firstWorkInProgressHook
workInProgressHook = firstWorkInProgressHook;
ReactDOM.render(<Counter/>, document.getElementById('root'));
}
render();
複製代碼
咱們來還原一下這個過程 你們看完應該瞭解,當下設置currentHook實際上是上個workInProgressHook經過next指針進行綁定獲取的,因此若是在條件語句中打破了調用順序,將會致使next指針指向出現誤差,這個時候你傳進去的setState是沒法正確改變對應的值,由於
各類自定義封裝的hooks =》react-use
第二次在掘金上發文,小陳也是react小菜🐔,但願能跟你們一塊兒討論學習,向高級前端架構進階!讓咱們一塊兒愛上fiber
如何以及爲何React Fiber使用鏈表遍歷組件樹
React Fiber架構
React 源碼解析 - reactScheduler 異步任務調度
展望 React 17,回顧 React 往事 全面 深刻
這多是最通俗的 React Fiber(時間分片) 打開方式=>調度策略
全面瞭解 React 新功能: Suspense 和 Hooks 生命週期
詳談 React Fiber 架構(1)