人人能讀懂的react源碼解析(大廠高薪必備)


人人都能讀懂的react源碼解析(大廠高薪必備)

2.react心智模型(來來來,讓大腦有react思維吧)

視頻講解

​ 視頻課程的目的是為了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,並且詳細debug源碼和分析,過程更清晰。

視頻講解(高效學習):點擊學習

課程結構:

  1. 開篇(聽說你還在艱難的啃react源碼)
  2. react心智模型(來來來,讓大腦有react思維吧)
  3. Fiber(我是在內存中的dom)
  4. 從legacy或concurrent開始(從入口開始,然后讓我們奔向未來)
  5. state更新流程(setState里到底發生了什么)
  6. render階段(厲害了,我有創建Fiber的技能)
  7. commit階段(聽說renderer幫我們打好標記了,映射真實節點吧)
  8. diff算法(媽媽再也不擔心我的diff面試了)
  9. hooks源碼(想知道Function Component是怎樣保存狀態的嘛)
  10. scheduler&lane模型(來看看任務是暫停、繼續和插隊的)
  11. concurrent mode(並發模式是什么樣的)
  12. 手寫迷你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 or yarn

  • 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
    


免責聲明!

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



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