人人都能讀懂的react源碼解析(大廠高薪必備)
2.react心智模型(來來來,讓大腦有react思維吧)
視頻講解
視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。
視頻講解(高效學習):點擊學習
課程結構:
- 開篇(聽說你還在艱難的啃react源碼)
- react心智模型(來來來,讓大腦有react思維吧)
- Fiber(我是在內存中的dom)
- 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
- state更新流程(setState里到底發生了什么)
- render階段(厲害了,我有創建Fiber的技能)
- commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
- diff算法(媽媽再也不擔心我的diff面試了)
- hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
- scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
- concurrent mode(並發模式是什么樣的)
- 手寫迷你react(短小精悍就是我)
在正式開始之前需要了解一下前置知識,現在不太清楚沒關系,這些內容會在后面的章節中出現並且詳細介紹,這一章的目標是了解react源碼中存在的模型(數據結構或者思想)
react架構
react的核心可以用ui=fn(state)來表示,更詳細可以用
const state = reconcile(update);
const UI = commit(state);
react源碼可以分為如下幾個模塊:
-
Scheduler(調度器): 排序優先級,讓優先級高的任務先進行reconcile
-
Reconciler(協調器): 找出哪些節點發生了改變,並打上不同的Tag
-
Renderer(渲染器): 將Reconciler中打好標簽的節點渲染到視圖上
Scheduler的作用是調度任務,react15沒有Scheduler這部分,所以所有任務沒有優先級,也不能中斷,只能同步執行。
Reconciler發生在render階段,render階段會分別為節點執行beginWork和completeWork(后面會講),或者計算state,對比節點的差異,為節點賦值相應的effectTag(對應dom節點的增刪改)
Renderer發生在commit階段,commit階段遍歷effectList執行對應的dom操作或部分生命周期
react17的出現是為了解決什么
react之前的版本在reconcile的過程中是同步執行的,而計算復雜組件的差異可能是一個耗時操作,加之js的執行是單線程的,設備性能不同,頁面就可能會出現卡頓的現象。此外應用所處的網絡狀況也不同,也需要應對不同網絡狀態下獲取數據的響應,所以為了解決這兩類(cpu、io)問題,react17帶了全新的concurrent mode,它是一類功能的合集(如fiber、schduler、lane、suspense),其目的是為了提高應用的響應速度,使應用不在那么卡頓,其核心是實現了一套異步可中斷、帶優先級的更新。
那么react17怎么實現異步可中斷的更新呢,我們知道一般瀏覽器的fps是60Hz,也就是每16.6ms會刷新一次,而js執行線程和GUI也就是瀏覽器的繪制是互斥的,因為js可以操作dom,影響最后呈現的結果,所以如果js執行的時間過長,會導致瀏覽器沒時間繪制dom,造成卡頓。react17會在每一幀分配一個時間(時間片)給js執行,如果在這個時間內js還沒執行完,那就要暫停它的執行,等下一幀繼續執行,把執行權交回給瀏覽器去繪制。
對比下開啟和未開啟concurrent mode的區別,開啟之后,構建Fiber的任務的執行不會一直處於阻塞狀態,而是分成了一個個的task
未開啟concurrent
開啟concurrent
Fiber雙緩存
Fiber(Virtual dom)是內存中用來描述dom階段的對象
在它上面保存了包括這個節點的屬性、類型、dom等,Fiber通過child、sibling、return(指向父節點)來形成Fiber樹,
還保存了更新狀態時用於計算state的updateQueue,updateQueue是一種鏈表結構,上面可能存在多個未計算的update,update也是一種數據結構,上面包含了更新的數據、優先級等,
除了這些之外,上面還有和副作用有關的信息。
雙緩存是指存在兩顆Fiber樹,current Fiber樹描述了當前呈現的dom樹,workInProgress Fiber是正在更新的Fiber樹,這兩顆Fiber樹都是在內存中運行的,在workInProgress Fiber構建完成之后會將它作為current Fiber應用到dom上
在mount時(首次渲染),會根據jsx對象(Class Component或的render函數者Function Component的返回值),構建Fiber對象,形成Fiber樹,然后這顆Fiber樹會作為current Fiber應用到真實dom上,在update(狀態更新時如setState)的時候,會根據狀態變更后的jsx對象和current Fiber做對比形成新的workInProgress Fiber,然后workInProgress Fiber切換成current Fiber應用到真實dom就達到了更新的目的,而這一切都是在內存中發生的,從而減少了對dom好性能的操作。
例如下面代碼的Fiber雙緩存結構如下,在第5章會詳細講解
function App() {
return (
<div>
xiao
<p>chen</p>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
Reconciler(render階段中)
協調器是在render階段工作的,簡單一句話概括就是Reconciler會創建或者更新Fiber節點。在mount的時候會根據jsx生成Fiber對象,在update的時候會根據最新的state形成的jsx對象和current Fiber樹對比構建workInProgress Fiber樹,這個對比的過程就是diff算法。reconcile時會在這些Fiber上打上Tag標簽,在commit階段把這些標簽應用到真實dom上,這些標簽代表節點的增刪改,如
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
render階段遍歷Fiber樹類似dfs的過程,‘捕獲’階段發生在beginWork函數中,該函數做的主要工作是創建Fiber節點,計算state和diff算法,‘冒泡’階段發生在completeWork中,該函數主要是做一些收尾工作,例如處理節點的props、和形成一條effectList的鏈表,該鏈表是被標記了更新的節點形成的鏈表
深度優先遍歷過程如下,圖中的數字是順序,return指向父節點,第8章詳細講解
如果p和h1節點更新了則effectList如下,從rootFiber->h1->p,,順便說下fiberRoot是整個項目的根節點,只存在一個,rootFiber是應用的根節點,可能存在多個,例如多個ReactDOM.render(<App />, document.getElementById("root"));
創建多個應用節點
Renderer(commit階段中)
Renderer是在commit階段工作的,commit階段會遍歷render階段形成的effectList,並執行真實dom節點的操作和一些生命周期,不同平台對應的Renderer不同,例如瀏覽器對應的就是react-dom。
commit階段發生在commitRoot函數中,該函數主要遍歷effectList,分別用三個函數來處理effectList上的節點,這三個函數是commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects,他們主要做的事情如下,后面會詳細講解,現在在大腦里有一個結構就行
diff算法
diff算法發生在render階段的reconcileChildFibers函數中,diff算法分為單節點的diff和多節點的diff(例如一個節點中包含多個子節點就屬於多節點的diff),單節點會根據節點的key和type,props等來判斷節點是復用還是直接新創建節點,多節點diff會涉及節點的增刪和節點位置的變化,詳細見第10章。
Scheduler
我們知道了要實現異步可中斷的更新,需要瀏覽器指定一個時間,如果沒有時間剩余了就需要暫停任務,requestIdleCallback貌似是個不錯的選擇,但是它存在兼容和觸發不穩定的原因,react17中采用MessageChannel來實現。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {//shouldYield判斷是否暫停任務
workInProgress = performUnitOfWork(workInProgress);
}
}
在Scheduler中的每的每個任務的優先級使用過期時間表示的,如果一個任務的過期時間離現在很近,說明它馬上就要過期了,優先級很高,如果過期時間很長,那它的優先級就低,沒有過期的任務存放在timerQueue中,過期的任務存放在taskQueue中,timerQueue和timerQueue都是小頂堆,所以peek取出來的都是離現在時間最近也就是優先級最高的那個任務,然后優先執行它。
Lane
react之前的版本用expirationTime
屬性代表優先級,該優先級和IO不能很好的搭配工作(io的優先級高於cpu的優先級),現在有了更加細粒度的優先級表示方法Lane,Lane用二進制位表示優先級,二進制中的1表示位置,同一個二進制數可以有多個相同優先級的位,這就可以表示‘批’的概念,而且二進制方便計算。
這好比賽車比賽,在比賽開始的時候會分配一個賽道,比賽開始之后大家都會搶內圈的賽道(react中就是搶優先級高的Lane),比賽的尾聲,最后一名賽車如果落后了很多,它也會跑到內圈的賽道,最后到達目的地(對應react中就是飢餓問題,低優先級的任務如果被高優先級的任務一直打斷,到了它的過期時間,它也會變成高優先級)
Lane的二進制位如下,從上往下,優先級遞減
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
jsx
jsx是ClassComponent的render函數或者FunctionComponent的返回值,可以用來表示組件的內容,在經過babel編譯之后,最后會被編譯成React.createElement
,這就是為什么jsx文件要聲明import React from 'react'
的原因,你可以在 babel編譯jsx 站點查看jsx被編譯后的結果
React.createElement
的源碼中做了如下幾件事
- 處理config,把除了保留屬性外的其他config賦值給props
- 把children處理后賦值給props.children
- 處理defaultProps
- 調用ReactElement返回一個jsx對象
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
//處理config,把除了保留屬性外的其他config賦值給props
//...
}
const childrenLength = arguments.length - 2;
//把children處理后賦值給props.children
//...
//處理defaultProps
//...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,//表示是ReactElement類型
type: type,//class或function
key: key,//key
ref: ref,//useRef的ref對象
props: props,//props
_owner: owner,
};
return element;
};
$$typeof表示的是組件的類型,例如在源碼中有一個檢查是否是合法Element的函數,就是根object.$$typeof === REACT_ELEMENT_TYPE來判斷的
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
如果組件是ClassComponent則type是class本身,如果組件是FunctionComponent創建的,則type是這個function,源碼中用ClassComponent.prototype.isReactComponent來區別二者。注意class或者function創建的組件一定要首字母大寫,不然后被當成普通節點,type就是字符串。
jsx對象上沒有優先級、狀態、effectTag等標記,這些標記在Fiber對象上,在mount時Fiber根據jsx對象來構建,在update是根據最新狀態的jsx和current Fiber對比,形成新的workInProgress Fiber,最后workInProgress Fiber切換成current Fiber
源碼目錄結構
源碼中主要包括如下部分
- fixtures:為代碼貢獻者提供的測試React
- packages:主要部分,包含Scheduler,reconciler等
- scripts:react構建相關
下面來看下packages主要包含的模塊
-
react:核心Api如:React.createElement、React.Component都在這
-
和平台相關render相關的文件夾:
react-art:如canvas svg的渲染
react-dom:瀏覽器環境
react-native-renderer:原生相關
react-noop-renderer:調試或者fiber用 -
試驗性的包
react-server: ssr相關
react-fetch: 請求相關
react-interactions: 和事件如點擊事件相關
react-reconciler: 構建節點
-
shared:包含公共方法和變量
-
輔助包:
react-is : 判斷類型
react-client: 流相關
react-fetch: 數據請求相關
react-refresh: 熱加載相關
-
scheduler:調度器相關
-
React-reconciler:在render階段用它來構建fiber節點
怎樣調試源碼
本課程使用的react版本是17.0.1,通過下面幾步就可以調試源碼了,當然你可以用現成的包含本課程所有demo的項目來調試,建議使用已經構建好的項目,地址:https://github.com/xiaochen1024/react_code_build
-
clone源碼:
git clone https://github.com/facebook/react.git
-
依賴安裝:
npm install
oryarn
-
build源碼:
npm build react,react-dom,scheduler --type=NODE
-
為源碼建立軟鏈:
cd build/node_modules/react npm link cd build/node_modules/react-dom npm link
-
create-react-app創建項目
npx create-react-app demo npm link react react-dom