react17.x源碼解析(1)——源碼目錄及react架構


react的源碼目錄如下,主要有三個文件夾:

  • fixtures:一些測試demo,方便react編碼時的測試
  • packages: react的主要源碼內容
  • script: 和react打包、編譯、本地開發相關的命令

我們要探究的源碼內容,都存放在packages文件夾下:

image

根據packages下面各個部分的功能,我將其划分為了幾個模塊:

核心 api
react的核心api都位於packages/react文件夾下,包括createElement、memo、context以及hooks等,凡是通過react包引入的api,都位於此文件夾下。

調度和協調
調度和協調是 react16 fiber出現后的核心功能,和他們相關的包如下:

  • scheduler:對任務進行調度,根據優先級排序
  • react-conciler:diff算法相關,對fiber進行副作用標記
    image

渲染
和渲染相關的內容包括以下幾個目錄:

  • 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(渲染器):

image

觸發更新
觸發更新的方式主要有以下幾種: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所做的事情如下:
image

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的動作:
image

通過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鏈表樹結構:
    image

副作用相關屬性
首先理解一下react中的副作用,舉一個生活中比較通俗的例子:我們感冒了本來吃點葯就沒事了,但是吃了葯身體過敏了,而這個過敏就是副作用。react中,我們修改了state、props、ref等數據,除了數據改變之外,還引起dom的變化,這種render階段不能完成的工作,我們稱之為副作用。

flags
react中通過flags記錄每個節點diff后需要變更的狀態,例如dom的添加、替換、刪除等。
image

Effect List
在render階段,react會采用深度優化先遍歷,對fiber數進行遍歷,把每一個副作用的fiber篩選出來,最后構建生成一個只帶副作用的Effect list 鏈表。和該鏈表相關的字段有firstEffect、nextEffect和lastEffect:
image

firstEffect指向第一個有副作用的fiber節點,lastEffect指向最后一個有副作用的節點,中間的節點全部通過nextEffect鏈接,最終形成Effect鏈表。

在commit階段,React拿到Effect list 鏈表中的數據后,根據每一個fiber節點的flags類型,對相應的DOM進行更改。

其它
其它需要重點關注一下的屬性還有lane和alternate。

lane
lane代表react要執行的fiber任務的優先級,通過這個字段,render階段react確定應該優先將哪些任務提交到commit階段去執行。

我們看一下源碼中lane的枚舉值:
image

同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節點。

參考:https://juejin.cn/post/7016512949330116645


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM