剖析 React 源碼:先熱個身

這是個人 React 源碼解讀課的第一篇文章,首先來講說爲啥要寫這個系列文章:html

  • 如今工做中基本都用 React 了,由此想了解下內部原理
  • 市面上 Vue 的源碼解讀數不勝數,可是反觀 React 相關的卻寥寥無幾,也是由於 React 源碼難度較高,所以我想來攻克這個難題
  • 本身以爲看懂並不必定看懂了,寫出來讓讀者看懂纔是真懂了,所以我要把我讀懂的東西寫出來

這個系列文章預計篇數會超過十篇,React 版本爲 16.8.6,如下是本系列文章你必須須要注意的地方:前端

  • 這是一門進階課,若是涉及到你不清楚的內容,請自行谷歌,另外最好具有 React 的開發能力
  • 這是一門講源碼的課,只閱讀是不大可能真正讀懂的,須要輔以 Demo 和 Debug 才能真正理解代碼的用途
  • 我 fork 了一份 16.8.6 版本的代碼,而且會爲讀過的代碼加上詳細的中文註釋。等不及我文章的同窗能夠先行閱讀 個人倉庫而且在閱讀本系列文章的時候也請跟着閱讀我註釋的代碼。由於版本不一樣可能會致使代碼不一樣,而且我不會在文章中貼上大段的代碼,只會對部分代碼作更詳細的解釋,其餘的代碼能夠跟着個人註釋閱讀
  • 閱讀源碼最早遇到的問題會是不知道該從何開始,我這份代碼註釋能夠幫助你們解決這個問題,你只須要跟着個人 commit 閱讀便可
  • 不會對任何 DEV 環境下的代碼作解讀,不會對全部代碼進行解讀,只會解讀核心功能(即便這樣也會是一個大工程)
  • 最後再說起一遍,請務必文章和 代碼 相結合來看,爲了篇幅考慮我不會將全部的代碼都貼上來,我拷貝的累,讀者看的也累

這篇文章內容不會很難,先給你們熱個身,請你們打開 個人代碼 並定位到 react 文件夾下的 src,這個文件夾也就是 React 的入口文件夾了。react

開始進入正文前先說下這個系列中個人行文思路:1. 代碼儘可能經過圖片展現,既美觀又方便閱讀,反正不須要你們複製代碼。2. 文章中只會講我認爲重要或者有意思的代碼,對於其餘代碼請自行閱讀個人倉庫,反正已經註釋好代碼了。3. 對於流程長的函數調用會使用流程圖的方式來總結。4. 不會幹巴巴的只講代碼,會結合實際來聊聊這些 API 能幫助咱們解決什麼問題。 git

文章相關資料

React.createElement

你們在寫 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 並把內建的幾個屬性(好比 refkey)剔除後丟到 props 對象中。dom

接下里是一段對於 children 的操做

首先把第二個參數以後的參數取出來,而後判斷長度是否大於一。大於一的話就表明有多個 children,這時候 props.children 會是一個數組,不然的話只是一個對象。所以咱們須要注意在對 props.children 進行遍歷的時候要注意它是不是數組,固然你也能夠利用 React.Children 中的 API,下文中也會對 React.Children 中的 API 進行講解。

最後就是返回了一個 ReactElement 對象

內部代碼很簡單,核心就是經過 ?typeof 來幫助咱們識別這是一個 ReactElement,後面咱們能夠看到不少這樣相似的類型。另外咱們須要注意一點的是:經過 JSX寫的 <APP /> 表明着 ReactElementAPP 表明着 React Component。

如下是這一小節的流程圖內容:

ReactBaseClasses

上文中講到了 APP 表明着 React Component,那麼這一小節咱們就來閱讀組件相關也就是 ReactBaseClasses.js 文件下的代碼。

其實在閱讀這部分源碼以前,我覺得代碼會很複雜,可能包含了不少組件內的邏輯,結果內部代碼至關簡單。這是由於 React 團隊將複雜的邏輯所有丟在了 react-dom 文件夾中,你能夠把 react-dom 當作是 React 和 UI 之間的膠水層,這層膠水能夠兼容不少平臺,好比 Web、RN、SSR 等等。

該文件包含兩個基本組件,分別爲 ComponentPureComponent,咱們先來閱讀 Component 這部分的代碼。

構造函數 Component 中須要注意的兩點分別是 refsupdater,前者會在下文中專門介紹,後者是組件中至關重要的一個屬性,咱們能夠發現 setStateforceUpdate 都是調用了 updater 中的方法,可是 updater 是 react-dom 中的內容,咱們會在以後的文章中學習到這部分的內容。

另外 ReactNoopUpdateQueue 也有一個單獨的文件,可是內部的代碼看不看都無所謂,由於都是用於報警告的。

接下來咱們來閱讀 PureComponent 中的代碼,其實這部分的代碼基本與 Component 一致

PureComponent 繼承自 Component,繼承方法使用了很典型的寄生組合式。

另外這兩部分代碼你能夠發現每一個組件都有一個 isXXXX 屬性用來標誌自身屬於什麼組件。

以上就是這部分的代碼,接下來的一小節咱們將會學習到 refs 的一部份內容。

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>
))
複製代碼

ReactChildren

這一小節會是這篇文章中最複雜的一部分,可能須要本身寫個 Demo 而且 Debug 一下才能真正理解源碼爲何要這樣實現。

首先你們須要定位到 ReactChildren.js 文件,這部分代碼中我只會介紹關於 mapChildren 函數相關的內容,由於這部分代碼基本就貫穿了整個文件了。

若是你沒有使用過這個 API,能夠先自行閱讀 文檔

對於 mapChildren 這個函數來講,一般會使用在組合組件設計模式上。若是你不清楚什麼是組合組件的話,能夠看下 Ant-design,它內部大量使用了這種設計模式,好比說 Radio.GroupRadio.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 內部究竟是如何實現的。

這段代碼有意思的部分是引入了對象重用池的概念,分別對應 getPooledTraverseContextreleaseTraverseContext 中的代碼。固然這個概念的用處其實很簡單,就是維護一個大小固定的對象重用池,每次從這個池子裏取一個對象去賦值,用完了就將對象上的屬性置空而後丟回池子。維護這個池子的用意就是提升性能,畢竟頻繁建立銷燬一個有不少屬性的對象會消耗性能。

接下來咱們來學習 traverseAllChildrenImpl 中的代碼,這部分的代碼須要分爲兩塊來說

這部分的代碼相對來講簡單點,主體就是在判斷 children 的類型是什麼。若是是能夠渲染的節點的話,就直接調用 callback,另外你還能夠發如今判斷的過程當中,代碼中有使用到 ?typeof 去判斷的流程。這裏的 callback 指的是 mapSingleChildIntoContext 函數,這部分的內容會在下文中說到。

這部分的代碼首先會判斷 children 是否爲數組。若是爲數組的話,就遍歷數組並把其中的每一個元素都遞歸調用 traverseAllChildrenImpl,也就是說必須是單個可渲染節點才能夠執行上半部分代碼中的 callback

若是不是數組的話,就看看 children 是否能夠支持迭代,原理就是經過 obj[Symbol.iterator] 的方式去取迭代器,返回值若是是個函數的話就表明支持迭代,而後邏輯就和以前的同樣了。

講完了 traverseAllChildrenImpl 函數,咱們最後再來閱讀下 mapSingleChildIntoContext 函數中的實現。

bookKeeping 就是咱們從對象池子裏取出來的東西,而後調用 func 而且傳入節點(此時這個節點確定是單個節點),此時的 func 表明着 React.mapChildren 中的第二個參數。

接下來就是判斷返回值類型的過程:若是是數組的話,仍是迴歸以前的代碼邏輯,注意這裏傳入的 funcc => c,由於要保證最終結果是被攤平的;若是不是數組的話,判斷返回值是不是一個有效的 Element,驗證經過的話就 clone 一份而且替換掉 key,最後把返回值放入 result 中,result 其實也就是 mapChildren 的返回值。

至此,mapChildren 函數相關的內容已經解析完畢,還不怎麼清楚的同窗能夠經過如下的流程圖再複習一遍。

其他內容

前面幾小節的內容已經把 react 文件夾下大部分有意思的代碼都講完了,其餘就剩餘了一些邊邊角角的內容。好比 memocontexthookslazy,這部分代碼有興趣的能夠直接自行閱讀,反正內容都仍是很簡單的,難的部分都在 react-dom 文件夾中。

其餘文章列表

最後

閱讀源碼是一個很枯燥的過程,可是收益也是巨大的。若是你在閱讀的過程當中有任何的問題,都歡迎你在評論區與我交流,固然你也能夠在倉庫中提 Issus。

另外寫這系列是個很耗時的工程,須要維護代碼註釋,還得把文章寫得儘可能讓讀者看懂,最後還得配上畫圖,若是你以爲文章看着還行,就請不要吝嗇你的點贊。

下一篇文章就會是 Fiber 相關的內容,而且會分紅幾篇文章來說解。

最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。

相關文章
相關標籤/搜索