這是個人 React 源碼解讀課的第一篇文章,首先來講說爲啥要寫這個系列文章:html
這個系列文章預計篇數會超過十篇,React 版本爲 16.8.6,如下是本系列文章你必須須要注意的地方:前端
這篇文章內容不會很難,先給你們熱個身,請你們打開 個人代碼 並定位到 react 文件夾下的 src,這個文件夾也就是 React 的入口文件夾了。react
開始進入正文前先說下這個系列中個人行文思路:1. 代碼儘可能經過圖片展現,既美觀又方便閱讀,反正不須要你們複製代碼。2. 文章中只會講我認爲重要或者有意思的代碼,對於其餘代碼請自行閱讀個人倉庫,反正已經註釋好代碼了。3. 對於流程長的函數調用會使用流程圖的方式來總結。4. 不會幹巴巴的只講代碼,會結合實際來聊聊這些 API 能幫助咱們解決什麼問題。 git
你們在寫 React 代碼的時候確定寫過 JSX,可是爲何一旦使用 JSX 就必須引入 React 呢?github
這是由於咱們的 JSX 代碼會被 Babel 編譯爲 React.createElement
,不引入 React 的話就不能使用 React.createElement
了。設計模式
<div id='1'>1</div>
// 上面的 JSX 會被編譯成這樣
React.createElement("div", {
id: "1"
}, "1")
複製代碼
那麼咱們就先定位到 ReactElement.js 文件閱讀下 createElement
函數的實現api
export function createElement(type, config, children) {} 複製代碼
首先 createElement
函數接收三個參數,具體表明着什麼相信你們能夠經過上面 JSX 編譯出來的東西自行理解。數組
而後是對於 config
的一些處理:markdown
這段代碼對 ref
以及 key
作了個驗證(對於這種代碼就無須閱讀內部實現,經過函數名就能夠了解它想作的事情),而後遍歷 config
並把內建的幾個屬性(好比 ref
和 key
)剔除後丟到 props 對象中。dom
接下里是一段對於 children
的操做
首先把第二個參數以後的參數取出來,而後判斷長度是否大於一。大於一的話就表明有多個 children
,這時候 props.children
會是一個數組,不然的話只是一個對象。所以咱們須要注意在對 props.children
進行遍歷的時候要注意它是不是數組,固然你也能夠利用 React.Children
中的 API,下文中也會對 React.Children
中的 API 進行講解。
最後就是返回了一個 ReactElement
對象
內部代碼很簡單,核心就是經過 ?typeof
來幫助咱們識別這是一個 ReactElement
,後面咱們能夠看到不少這樣相似的類型。另外咱們須要注意一點的是:經過 JSX寫的 <APP />
表明着 ReactElement
,APP
表明着 React Component。
如下是這一小節的流程圖內容:
上文中講到了 APP
表明着 React Component,那麼這一小節咱們就來閱讀組件相關也就是 ReactBaseClasses.js 文件下的代碼。
其實在閱讀這部分源碼以前,我覺得代碼會很複雜,可能包含了不少組件內的邏輯,結果內部代碼至關簡單。這是由於 React 團隊將複雜的邏輯所有丟在了 react-dom 文件夾中,你能夠把 react-dom 當作是 React 和 UI 之間的膠水層,這層膠水能夠兼容不少平臺,好比 Web、RN、SSR 等等。
該文件包含兩個基本組件,分別爲 Component
及 PureComponent
,咱們先來閱讀 Component
這部分的代碼。
構造函數 Component
中須要注意的兩點分別是 refs
和 updater
,前者會在下文中專門介紹,後者是組件中至關重要的一個屬性,咱們能夠發現 setState
和 forceUpdate
都是調用了 updater
中的方法,可是 updater
是 react-dom 中的內容,咱們會在以後的文章中學習到這部分的內容。
另外 ReactNoopUpdateQueue
也有一個單獨的文件,可是內部的代碼看不看都無所謂,由於都是用於報警告的。
接下來咱們來閱讀 PureComponent
中的代碼,其實這部分的代碼基本與 Component
一致
PureComponent
繼承自 Component
,繼承方法使用了很典型的寄生組合式。
另外這兩部分代碼你能夠發現每一個組件都有一個 isXXXX
屬性用來標誌自身屬於什麼組件。
以上就是這部分的代碼,接下來的一小節咱們將會學習到 refs
的一部份內容。
refs 其實有好幾種方式能夠建立:
ref={el => this.el = el}
React.createRef
這一小節咱們來學習 React.createRef
相關的內容,其他的兩種方式不在這篇文章的討論範圍以內,請先定位到 ReactCreateRef.js 文件。
內部實現很簡單,若是咱們想使用 ref
,只須要取出其中的 current
對象便可。
另外對於函數組件來講,是不能使用 ref
的,若是你不知道緣由的話能夠直接閱讀 文檔。
固然在以前也是有取巧的方式的,就是經過 props
的方式傳遞 ref
,可是如今咱們有了新的方式 forwardRef
去解決這個問題。
具體代碼見 forwardRef.js 文件,一樣內部代碼仍是很簡單
這部分代碼最重要的就是咱們能夠在參數中得到 ref
了,所以咱們若是想在函數組件中使用 ref
的話就能夠把代碼寫成這樣:
const FancyButton = React.forwardRef((props, ref) => ( <button ref={ref} className="FancyButton"> {props.children} </button> )) 複製代碼
這一小節會是這篇文章中最複雜的一部分,可能須要本身寫個 Demo 而且 Debug 一下才能真正理解源碼爲何要這樣實現。
首先你們須要定位到 ReactChildren.js 文件,這部分代碼中我只會介紹關於 mapChildren
函數相關的內容,由於這部分代碼基本就貫穿了整個文件了。
若是你沒有使用過這個 API,能夠先自行閱讀 文檔。
對於 mapChildren
這個函數來講,一般會使用在組合組件設計模式上。若是你不清楚什麼是組合組件的話,能夠看下 Ant-design,它內部大量使用了這種設計模式,好比說 Radio.Group
、Radio.Button
,另外這裏也有篇 文檔 介紹了這種設計模式。
咱們先來看下這個函數的一些神奇用法
React.Children.map(this.props.children, c => [[c, c]]) 複製代碼
對於上述代碼,map
也就是 mapChildren
函數來講返回值是 [c, c, c, c]
。無論你第二個參數的函數返回值是幾維嵌套數組,map
函數都能幫你攤平到一維數組,而且每次遍歷後返回的數組中的元素個數表明了同一個節點須要複製幾回。
若是文字描述有點難懂的話,就來看代碼吧:
<div> <span>1</span> <span>2</span> </div> 複製代碼
對於上述代碼來講,經過 c => [[c, c]]
轉換之後就變成了
<span>1</span> <span>1</span> <span>2</span> <span>2</span> 複製代碼
接下里咱們進入正題,來看看 mapChildren
內部究竟是如何實現的。
這段代碼有意思的部分是引入了對象重用池的概念,分別對應 getPooledTraverseContext
和 releaseTraverseContext
中的代碼。固然這個概念的用處其實很簡單,就是維護一個大小固定的對象重用池,每次從這個池子裏取一個對象去賦值,用完了就將對象上的屬性置空而後丟回池子。維護這個池子的用意就是提升性能,畢竟頻繁建立銷燬一個有不少屬性的對象會消耗性能。
接下來咱們來學習 traverseAllChildrenImpl
中的代碼,這部分的代碼須要分爲兩塊來說
這部分的代碼相對來講簡單點,主體就是在判斷 children
的類型是什麼。若是是能夠渲染的節點的話,就直接調用 callback
,另外你還能夠發如今判斷的過程當中,代碼中有使用到 ?typeof
去判斷的流程。這裏的 callback
指的是 mapSingleChildIntoContext
函數,這部分的內容會在下文中說到。
這部分的代碼首先會判斷 children
是否爲數組。若是爲數組的話,就遍歷數組並把其中的每一個元素都遞歸調用 traverseAllChildrenImpl
,也就是說必須是單個可渲染節點才能夠執行上半部分代碼中的 callback
。
若是不是數組的話,就看看 children
是否能夠支持迭代,原理就是經過 obj[Symbol.iterator]
的方式去取迭代器,返回值若是是個函數的話就表明支持迭代,而後邏輯就和以前的同樣了。
講完了 traverseAllChildrenImpl
函數,咱們最後再來閱讀下 mapSingleChildIntoContext
函數中的實現。
bookKeeping
就是咱們從對象池子裏取出來的東西,而後調用 func
而且傳入節點(此時這個節點確定是單個節點),此時的 func
表明着 React.mapChildren
中的第二個參數。
接下來就是判斷返回值類型的過程:若是是數組的話,仍是迴歸以前的代碼邏輯,注意這裏傳入的 func
是 c => c
,由於要保證最終結果是被攤平的;若是不是數組的話,判斷返回值是不是一個有效的 Element,驗證經過的話就 clone 一份而且替換掉 key
,最後把返回值放入 result
中,result
其實也就是 mapChildren
的返回值。
至此,mapChildren
函數相關的內容已經解析完畢,還不怎麼清楚的同窗能夠經過如下的流程圖再複習一遍。
前面幾小節的內容已經把 react 文件夾下大部分有意思的代碼都講完了,其餘就剩餘了一些邊邊角角的內容。好比 memo
、context
、hooks
、lazy
,這部分代碼有興趣的能夠直接自行閱讀,反正內容都仍是很簡單的,難的部分都在 react-dom 文件夾中。
閱讀源碼是一個很枯燥的過程,可是收益也是巨大的。若是你在閱讀的過程當中有任何的問題,都歡迎你在評論區與我交流,固然你也能夠在倉庫中提 Issus。
另外寫這系列是個很耗時的工程,須要維護代碼註釋,還得把文章寫得儘可能讓讀者看懂,最後還得配上畫圖,若是你以爲文章看着還行,就請不要吝嗇你的點贊。
下一篇文章就會是 Fiber 相關的內容,而且會分紅幾篇文章來說解。
最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。