react的源碼目錄如下,主要有三個文件夾:
- fixtures:一些測試demo,方便react編碼時的測試
- packages: react的主要源碼內容
- script: 和react打包、編譯、本地開發相關的命令
我們要探究的源碼內容,都存放在packages文件夾下:
根據packages下面各個部分的功能,我將其划分為了幾個模塊:
核心 api
react的核心api都位於packages/react文件夾下,包括createElement、memo、context以及hooks等,凡是通過react包引入的api,都位於此文件夾下。
調度和協調
調度和協調是 react16 fiber出現后的核心功能,和他們相關的包如下:
- scheduler:對任務進行調度,根據優先級排序
- react-conciler:diff算法相關,對fiber進行副作用標記
渲染
和渲染相關的內容包括以下幾個目錄:
- react-art:canvas、svg等內容的渲染
- react-dom:瀏覽器環境下的渲染,也是我們本系列中主要涉及講解的渲染的包
- react-native-renderer: 用於原生環境渲染相關
- react-noop-renderer: 用於調試環境的渲染
輔助包
- shared:定義了react的公共方法和變量
- react-is:react中的類型判斷
其他
其他的包和本次react源碼探究的關聯不是很多,不過多介紹。
react架構
react為了保證頁面能夠流暢渲染,react16之后的更新過程分為render和commit兩個階段。render階段包括Scheduler(調度器)和Reconciler(協調器),commit階段包括Renderer(渲染器):
觸發更新
觸發更新的方式主要有以下幾種:ReactDOM.render(包括首次渲染)、setState、forUpdate、hooks中的useState以及ref的改變等引起的。
scheduler
當首次渲染或組件狀態發生更新等情況時,此時頁面就要發生渲染了。scheduler過程會對諸多的任務進行優先級排序,讓瀏覽器的每一幀優先執行高優先級的任務(例如動畫、用戶點擊輸入事件等),從而防止react的更新任務太大影響到用戶交互,保證了頁面的流暢性。
reconciler
reconciler過程中,會開始根據優先級執行更新任務。這一過程主要是根據最新狀態構建新的fiber樹,與之前的fiber樹進行diff對比,對fiber節點標記不同的副作用,對渲染過程中真實dom的增刪改。
commit
在render階段中,最終會生成一個effectList數組,記錄了頁面真實dom的新增、刪除和替換等以及一些事件響應,commit會根據effectList對真實的頁面進行更新,從而實現頁面的改變。
jsx的轉換
在React16版本及之前,應用程序通過@babel/preset-react將jsx語法轉換為React.createElement的js代碼,因此需要顯示將React引入,才能正常調用createElement。
React17版本之后,官方與babel進行了合作,直接通過將react/jsx-runtime對jsx語法進行了新的轉換而不依賴React.createElement,轉換的結果便是可直接供ReactDOM.render使用的ReactElement對象。因此如果在React17版本后只是用jsx語法不使用其它的react提供的api,可以不引入React,應用程序依然能夠正常運行。
React.createElement源碼
雖然現在react17之后我們可以不再依賴React.createElement這個api了,但是實際場景中以及很多開源包中可能會有很多通過React.createElement手動創建元素的場景,所以推薦學習一下Reat.createElement源碼。
React.createElement其接收三個或以上參數:
- type:要創建的React元素類型,可以是標簽名稱字符串,如'div'或者'span'等;也可以說是React組件類型(class組件或者函數組件);或者是React fragment類型。
- config:寫在標簽上的屬性的集合,js對象格式,若標簽上未添加任何屬性則為null。
- children:從第三個參數開始后的參數為當前創建的React元素的子節點,每個參數的類型,若是當前元素節點的textContent則為字符串類型;否則為新的React.createElement創建的元素。
函數中會對參數進行一系列的解析,源碼如下,對源碼相關的理解都用注釋進行了標記:
export function createElement(type,config,children){
let propName;
//記錄標簽上的屬性集合
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
//config不為null時,說明標簽上有屬性,將屬性添加到props中
//其中,key和ref為react提供的特殊屬性,不加入到props中,而是用key和ref單獨記錄
if(config !=null){
if(hasValidRef(config)){
//有合法的ref時,則給ref賦值
ref = config.ref;
if(__DEV__){
warnIfStringRefCannotBeAutoConverted(config);
}
}
if(hasValidKey(config)){
//有合法的key時,則給key賦值
key = '' + config.key;
}
//self和source是開發環境下對代碼在編譯器中位置信息進行記錄,用於開發環境下調試
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 將config中除key、ref、__self、__source之外的屬性添加到props中
for(propName in config){
if(
hasOwnProperty.call(config,propName)&&
!RESERVED_PROPS.hasOwnProperty(propName)
){
props[propName] = config[propName];
}
}
}
//將子節點添加到props的children屬性上
const childrenLength = arguments.length -2;
if(childrenLength===1){
//共3個參數時表示只有一個子節點,直接將子節點賦值給props的children屬性
props.children = children;
}else if (childrenLength > 1){
//3個以上參數時表示有多個子節點,將子節點push到一個數組中然后將數組賦值給props的children
const childArray = Array(childrenLength);
for(let i=0;i<childrenLength;i++){
childArray[i] = arguments[i+2];
}
//開發環境下凍結 childArray,防止被隨意修改
if(__DEV__){
if(Object.freeze){
Object.freeze(childArray);
}
}
props.children = childArray;
}
//如果有defaultProps,對其遍歷並且將用戶在標簽上未對其手動設置屬性添加props中
//此處針對class組件類型
if(type && type.defaultProps){
const defaultProps = type.defaultProps;
for(propName in defaultProps){
if(props[propName]===undefined){
props[porpName] = defaultProps[propName];
}
}
}
// key 和 ref不掛載到prps上
// 開發環境若想通過props.key 或者props.ref獲取warning
if(__DEV__){
if(key || ref){
const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
if(key){
defineKeyPropWarningGetter(props,displayName);
}
if (ref){
defineRefPropWarningGetter(props,displayName);
}
}
}
// 調用 ReactElement並返回
return ReactElement(
type,
key,
self,
source,
ReactCurrentOwner.current,
props,
)
}
由此可知,React.createElement 做的事情主要有:
- 解析config參數中是否有合法的key、ref、__source和__self屬性,若存在分別賦值給key、ref、source和self;將剩余的屬性解析掛載到props上
- 除type和config外后面的參數,掛載到props.children上
- 針對類組件,如果type.defaultProps存在,遍歷type.defaultProps的屬性,如果props不存在該屬性,則添加到props上
- 將type、key、ref、self、props等信息,調用ReactElement等函數創建虛擬dom,ReactElement主要是在開發環境下通過Object.defineProperty將_store、_self、_source設置為不可枚舉,提高element比較時的性能:
const ReactElement = function(type,key,ref,self,source,owner,props){
const element = {
//用於表示是否為ReactElement
&&typeof:REACT_ELEMENT_TYPE,
// 用於創建真實 dom 的相關信息
type:type,
key:key,
ref:ref,
props:props,
_owner:owner,
};
if(__DEV__){
element._store = {};
//開發環境下將_store、_self、_source設置為不可枚舉,提高element的比較性能
Object.defineProperty(element._store,'validated',{
configurable:false,
enumerable:false,
writable:true,
value:false,
})
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
// 凍結 element 和 props,防止被手動修改
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
所以通過流程圖總結一下createElement所做的事情如下:
React.Component源碼
function Component(props,context,updater){
//接收props,context,updater進行初始化,掛載到this上
this.props = props;
this.context = context;
this.refs = emptyObject;
//updater 上掛載了isMounted、enqueueForceUpdate、enqueueSetState等觸發器方法
this.updater = updater || ReactNoopUpdateQueue;
}
//原型鏈上掛載 isReactComponent,在ReactDOM.render時用於和函數組件作區分
Component.prototype.isReactComponent = {};
//給類組件添加`this.setState`方法
Component.prototype.setState = function(partialState,callback){
//驗證參數是否合法
invariant(
typeof partialState === 'object' || typeof partialState === 'function' || partialState == null
);
//添加至 enqueueSetState隊列
this.updater.enqueueSetState(this,partialState,callback,'setState');
};
// 給類組件添加 `this.forceUpdate`方法
Component.prototype.forceUpdate = function(callback){
//添加至 enqueueForceUpdate隊列
this.updater.enqueueForceUpdate(this,callback,'forceUpdate');
}
從源碼上可以得知,React.Component主要做了以下幾件事情:
- 將props,context,updater掛載到this上
- 在Component原型鏈上添加isReactComponent對象,用於標記類組件
- 在Component原型鏈上添加setState方法
- 在Component原型鏈上添加forceUpdate方法
這樣我們就理解了react類組件的super()作用,以及this.setState和this.forceUpdate的由來
總結
react17之后babel對jsx的轉換就是比之前多了一步 React.createElement的動作:
通過babel及React.createElement,將jsx轉換為了瀏覽器能識別的原生js語法,為react后續對狀態改變、事件響應以及頁面更新奠定了基礎。
fiber節點結構
fiber是一種數據結構,每個fiber節點的內部,都保存了dom相關信息、fiber樹相關的引用、要更新時的副作用等,我們可以看下源碼中的fiber結構:
export type Fiber = {|
//作為靜態數據結構,存儲節點dom相關信息
tag:WorkTag,//組件的類型,取決於react的元素類型
key:null | string,
elementType:any,//元素類型
type:any,//定義與此fiber關聯的功能或類。對於組件,他指向構造函數;對於DOM元素,他指定HTML tag
stateNode:any,//真實dom節點
// fiber鏈表樹相關
return:Fiber|null,//父 fiber
child:Fiber|null,//第一個子fiber
sibling: Fiber | null,//下一個兄弟fiber
index:number,//在父fiber下面的子fiber中的下標
ref:
|null
|(((handle:mixed)=>void)&{_stringRef:?string,...})
|RefObject,
//工作單元,用於計算 state和props渲染
pendingProps:any,//本次渲染需要使用props
memoizedProps:any,//上次渲染使用的props
updateQueue:mixed,//用於狀態更新、回調函數、DOM更新的隊列
memoizedState:any,//上次渲染后的state狀態
dependencies:Dependencies | null, //contexts、events等依賴
mode:TypeOfMode,
//副作用相關
flags:Flags,//記錄更新時當前fiber的副作用(刪除、更新、替換等)狀態
subtreeFlags:Flags, //當前子樹的副作用狀態
deletions:Array<Fiber> | null, //要刪除的子fiber
nextEffet:Fiber | null,//下一個有副作用的fiber
firstEffect:Fiber | null,//指向第一個有副作用的fiber
lastEffectZ: Fiber | null,//指向最后一個有副作用的fiber
//優先級相關
lanes:Lanes,
childLanes:Lanes,
alternate:Fiber | null,//指向workInProgress fiber樹中對應的節點
actualDuration?:number,
actualStartTime?:number,
selfBaseDuration?:number,
treeBaseDuration?:number,
_debugID?:number,
_debugSource?:Source | null,
_debugOwner?:Fiber | null,
_debugIsCurrentlyTiming?:boolean,
_debugNeedsRemount?:bollean,
_debugHookTypes?:Array<HooKType> | null,
|};
dom相關屬性
fiber中和dom節點相關的信息主要關注tag、key、type、和stateNode。
tag
fiber中tag屬性的ts類型為workType,用於標記不同的react組件類型,我們可以看一下源碼中workType的枚舉值;
//packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent =2;
export const HostRoot = 3;
export const HostPortal = 4;
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const FundamentalComponent = 20;
export const ScopeComponent = 21;
export const Block = 22;
export const OffscreenComponent = 23;
export const LegacyHiddenComponent = 24;
在react協調時,beginWork和completeWork等流程時,都會根據tag類型的不同,去執行不同的函數處理fiber節點。
key和type
key和type兩項用於react diff過程中確定fiber是否可以復用。
key為用戶定義的唯一值。type定義與此fiber關聯的功能或類。對於組件,他指向函數或者類本身;對於DOM元素,他指定HTML tag。
stateNode
stateNode用於記錄當前fiber所對應的真實dom節點或者當前虛擬組件的實例,這么做的原因第一是為了實現Ref,第二是為了實現真實dom的跟蹤。
鏈表樹相關屬性
我們看一下和fiber鏈表樹構建相關的return、child和sibling幾個字段:
- return:指定父fiber,若沒有父fiber則為null
- child: 指向第一個子fiber,若沒有任何子fiber則為null
- sibling:指向下一個兄弟fiber,若沒有下一個兄弟fiber則為null
通過這幾個字段,各個fiber節點構成了fiber鏈表樹結構:
副作用相關屬性
首先理解一下react中的副作用,舉一個生活中比較通俗的例子:我們感冒了本來吃點葯就沒事了,但是吃了葯身體過敏了,而這個過敏就是副作用。react中,我們修改了state、props、ref等數據,除了數據改變之外,還引起dom的變化,這種render階段不能完成的工作,我們稱之為副作用。
flags
react中通過flags記錄每個節點diff后需要變更的狀態,例如dom的添加、替換、刪除等。
Effect List
在render階段,react會采用深度優化先遍歷,對fiber數進行遍歷,把每一個副作用的fiber篩選出來,最后構建生成一個只帶副作用的Effect list 鏈表。和該鏈表相關的字段有firstEffect、nextEffect和lastEffect:
firstEffect指向第一個有副作用的fiber節點,lastEffect指向最后一個有副作用的節點,中間的節點全部通過nextEffect鏈接,最終形成Effect鏈表。
在commit階段,React拿到Effect list 鏈表中的數據后,根據每一個fiber節點的flags類型,對相應的DOM進行更改。
其它
其它需要重點關注一下的屬性還有lane和alternate。
lane
lane代表react要執行的fiber任務的優先級,通過這個字段,render階段react確定應該優先將哪些任務提交到commit階段去執行。
我們看一下源碼中lane的枚舉值:
同Flags的枚舉值一樣,Lanes也是用31位的二進制數表示,表示了31條賽道,位數越小的賽道,代表的優先級越高。
例如 InputDiscreteHydrationLane、InputDiscreteLanes、InputContinuousHydrationLane等用戶交互引起的更新的優先級較高,DefaultLanes這種請求數據引起更新的優先級中等,而OffscreenLane、IdleLanes這種優先級較低。
優先級越低的任務,在render階段越容易被打斷,commit執行的時機越靠后。
alternate
當react的狀態發生更新時,當前頁面所對應的fiber樹稱為current Fiber,同時react會根據新的狀態構建一顆新的fiber樹,稱為 workInProgress Fiber。current Fiber中每個fiber節點通過alternate字段,指向workInProgress Fiber中對應的fiber節點。同樣workInProgress Fiber中的fiber節點的alternate字段也會指向current Fiber中對應的fiber節點。