人人都能讀懂的react源碼解析(大廠高薪必備)
11.concurrent mode(並發模式是什么樣的)
視頻課程&調試demos
視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。
視頻課程:進入課程
demos:demo
課程結構:
- 開篇(聽說你還在艱難的啃react源碼)
- react心智模型(來來來,讓大腦有react思維吧)
- Fiber(我是在內存中的dom)
- 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
- state更新流程(setState里到底發生了什么)
- render階段(厲害了,我有創建Fiber的技能)
- commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
- diff算法(媽媽再也不擔心我的diff面試了)
- hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
- scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
- concurrent mode(並發模式是什么樣的)
- 手寫迷你react(短小精悍就是我)
concurrent mode
react17開始支持concurrent mode,這種模式的根本目的是為了讓應用保持cpu和io的快速響應,它是一組新功能,包括Fiber、Scheduler、Lane,可以根據用戶硬件性能和網絡狀況調整應用的響應速度,核心就是為了實現異步可中斷的更新。concurrent mode也是未來react主要迭代的方向。
- cup:讓耗時的reconcile的過程能讓出js的執行權給更高優先級的任務,例如用戶的輸入,
- io:依靠Suspense
Fiber
Fiber我們之前介紹過,這里我們來看下在concurrent mode下Fiber的意義,react15之前的reconcile是同步執行的,當組件數量很多,reconcile時的計算量很大時,就會出現頁面的卡頓,為了解決這個問題就需要一套異步可中斷的更新來讓耗時的計算讓出js的執行權給高優先級的任務,在瀏覽器有空閑的時候再執行這些計算。所以我們需要一種數據結構來描述真實dom和更新的信息,在適當的時候可以在內存中中斷reconcile的過程,這種數據結構就是Fiber。
Scheduler
Scheduler獨立於react本身,相當於一個單獨的package,Scheduler的意義在於,當cup的計算量很大時,我們根據設備的fps算出一幀的時間,在這個時間內執行cup的操作,當任務執行的時間快超過一幀的時間時,會暫停任務的執行,讓瀏覽器有時間進行重排和重繪。在適當的時候繼續任務。
在js中我們知道generator也可以暫停和繼續任務,但是我們還需要用優先級來排列任務,這個是generator無法完成的。在Scheduler中使用MessageChannel實現了時間切片,然后用小頂堆排列任務優先級的高低,達到了異步可中斷的更新。
Scheduler可以用過期時間來代表優先級的高低。
優先級越高,過期時間越短,離當前時間越近,也就是說過一會就要執行它了。
優先級越低,過期時間越長,離當前時間越長,也就是過很久了才能輪到它執行。
lane
Lane用二進制位表示任務的優先級,方便優先級的計算,不同優先級占用不同位置的‘賽道’,而且存在批的概念,優先級越低,‘賽道’越多。高優先級打斷低優先級,新建的任務需要賦予什么優先級等問題都是Lane所要解決的問題。
batchedUpdates
簡單來說,在一個上下文中同時觸發多次更新,這些更新會合並成一次更新,例如
onClick() {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
在之前的react版本中如果脫離當前的上下文就不會被合並,例如把多次更新放在setTimeout中,原因是處於同一個context的多次setState的executionContext都會包含BatchedContext,包含BatchedContext的setState會合並,當executionContext等於NoContext,就會同步執行SyncCallbackQueue中的任務,所以setTimeout中的多次setState不會合並,而且會同步執行。
onClick() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
});
}
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext |= BatchedContext;//包含BatchedContext
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
resetRenderTimer();
//executionContext為NoContext就同步執行SyncCallbackQueue中的任務
flushSyncCallbackQueue();
}
}
}
在Concurrent mode下,上面的例子也會合並為一次更新,根本原因在如下一段簡化的源碼,如果多次setState,會比較這幾次setState回調的優先級,如果優先級一致,則先return掉,不會進行后面的render階段
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;//之前已經調用過的setState的回調
//...
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
//新的setState的回調和之前setState的回調優先級相等 則進入batchedUpdate的邏輯
if (existingCallbackPriority === newCallbackPriority) {
return;
}
cancelCallback(existingCallbackNode);
}
//調度render階段的起點
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
//...
}
那為什么在Concurrent mode下,在setTimeout回調多次setState優先級一致呢,因為在獲取Lane的函數requestUpdateLane,只有第一次setState滿足currentEventWipLanes === NoLanes,所以他們的currentEventWipLanes參數相同,而在findUpdateLane中schedulerLanePriority參數也相同(調度的優先級相同),所以返回的lane相同。
export function requestUpdateLane(fiber: Fiber): Lane {
//...
if (currentEventWipLanes === NoLanes) {//第一次setState滿足currentEventWipLanes === NoLanes
currentEventWipLanes = workInProgressRootIncludedLanes;
}
//...
//在setTimeout中schedulerLanePriority, currentEventWipLanes都相同,所以返回的lane也相同
lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
//...
return lane;
}
Suspense
Suspense可以在請求數據的時候顯示pending狀態,請求成功后展示數據,原因是因為Suspense中組件的優先級很低,而離屏的fallback組件優先級高,當Suspense中組件resolve之后就會重新調度一次render階段,此過程發生在updateSuspenseComponent函數中,具體可以看調試suspense的視頻