React源碼解析(一):組件的實現與掛載

當咱們可以熟練運用React進行前端開發時,難免會對React內部機制產生濃厚的興趣。組件是什麼?是真的DOM嗎?生命週期函數的執行依據又是什麼呢?前端

本篇,咱們先來研究React組件的實現與掛載。node

1.組件是什麼

首先編寫一個最簡單的組件:react

上述代碼寫完後,咱們就獲得了<A />這個組件,那麼咱們接下來先弄清楚<A />是什麼。用console.log打印出來:git

能夠看出,<A />實際上是js對象而不是真實的DOM,注意此時props是空對象。接下來,咱們打印<A><div>這是A組件</div></A>,看看控制檯會輸出什麼:github

咱們看到,props發生了變化,因爲<A />組件中嵌套了一個divdiv中又嵌套了文字,因此在描述<A />對象的props中增長了children屬性,其值爲描述div的js對象。同理,若是咱們進行多層的組件嵌套,其實就是在父對象的props中增長children字段及對應的描述值,也就是js對象的多層嵌套。typescript

以上描述是基於ES6的React開發模式,其實在ES5中經過React.createClass({})方法建立的組件,與ES6中是徹底同樣的,一樣能夠經過控制檯打印輸出組件結果進行驗證,此處再也不贅述。緩存

那麼形如HTML標籤實際上倒是對象的React組件是如何構成的呢?babel

由於咱們的組件聲明基於ReactComponent,因此首先咱們打開React.js,能夠看到以下代碼:app

咱們在import React from 'react'時,引入的就是源碼中提供的React對象。在extends Component時,繼承了Component類。這裏須要說明兩點:函數

  • 源碼中明明使用的module.exports而不是export default,爲何還可以成功引入呢?其實這是babel解析器的功勞。它令(ES6)import === (CommonJS)require。而在typescript中,須要嚴格的export default聲明,故在typescript下就不能使用import React from 'react'了,有興趣的讀者能夠嘗試一下。
  • 咱們能夠寫extends Component也能夠寫extends React.Component,這二者是否存在區別呢?答案是否認的。由於ComponentReact.Component的引用。也就是說Component === React.Component,在實際項目中寫哪一個均可以。

沿着ReactComponent的線索,咱們打開node_modules/react/lib/ReactComponent.js:

上述代碼是再熟悉不過的構造函數,想必你們已經倒背如流了。同時咱們也注意到setState是定義在原型上具備兩個參數的方法,具體原理咱們將在React更新機制的篇章講解。

上述代碼代表,咱們在最開始聲明的組件A,實際上是繼承ReactComponent類的子類,它的原型具備setState等方法。這樣組件A已經有了最基本的雛形。

小結

2.組件的初始化

聲明A後,咱們能夠在其內部自定義方法,也可使用生命週期的方法,如ComponentDidMount等等,這些和咱們在寫"類"的時候是徹底同樣的。惟一不一樣的是組件類必須擁有render方法輸出相似<div>這是A組件</div>的結構並掛載到真實DOM上,才能觸發組件的生命週期併成爲DOM樹的一部分。首先咱們觀察ES6的"類"是如何初始化一個react組件的。

將最初的示例代碼放入babel中:

其中_Component是對象ReactComponent_inherit方法是extends關鍵字的函數實現,這些都是ES6相關內容,咱們暫時無論。關鍵在於咱們發現render方法其實是調用了React.createElement方法(實際是ReactElement方法)。而後咱們打開ReactElement.js:

看到這裏咱們發現,其實每個組件對象都是經過React.createElement方法建立出來的ReactElement類型的對象。換句話說,ReactElment是一種內部記錄組件特徵並告訴React你想在屏幕上看到什麼的對象。 在ReactElement中:

參數 功能
$$typeof 組件的標識信息
key DOM結構標識,提高update性能
props 子結構相關信息(有則增長children字段/沒有爲空)和組件屬性(如style)
ref 真實DOM的引用
_owner _owner === ReactCurrentOwner.current(ReactCurrentOwner.js),值爲建立當前組件的對象,默認值爲null。

看完上述內容相信你們已經對React組件的實質有了必定的瞭解。經過執行React.createElement建立出的ReactElement類型的js對象,就是"React組件",這與控制檯打印出的結果徹底對應。總結來講,若是咱們經過class關鍵字聲明React組件,那麼他們在解析成真實DOM以前一直是ReactElement類型的js對象。

小結

對以前的思惟導圖進行補充:

3.組件的掛載

咱們知道能夠經過ReactDOM.render(component,mountNode)的形式對自定義組件/原生DOM/字符串進行掛載,

那麼掛載的過程又是如何實現的呢?

ReactDOM.render實際調用了內部的ReactMount.render,進而執行ReactMount._renderSubtreeIntoContainer。從字面意思上就能夠看出是將"子DOM"插入容器的邏輯,咱們看下源碼實現:

這段代碼很是重要,render函數的功能所有再在此(可點擊圖片大圖)。

咱們先來解析傳入_renderSubtreeIntoContainer的參數:

參數 功能
parentComponent 當前組件的父組件,第一次渲染時爲null
nextElement 要插入DOM中的組件,如helloWorld
container 要插入的容器,如document.getElementById('root')
callback 完成後的回調函數

這幾個參數的功能很好理解,接下來咱們逐行進行邏輯分析:

  • line 2:將當前組件添加到前一級的props屬性下。(本文開頭已說明父子嵌套關係由props提供)
  • line 4 ~ 22:調用getTopLevelWrapperInContainer方法判斷當前容器下是否存在組件,記爲prevComponent;若是有即prevComponenttrue,執行更新流程,即調用_updateRootComponent方法。若不存在,則卸載。(調用unmountComponentAtNode方法)
  • line 24:不論是更新仍是卸載,最終都要掛載到真實的DOM上。看下._renderNewRootComponent的源碼:

分析一下流程:

  • 第3行出現了instantiateReactComponent包裝方法,這個咱們後面再說。
  • 第5行中batchedMountComponentIntoNode以事務的形式調用mountComponentIntoNode(事務將專門拿出一篇文章來解析),該方法返回組件對應的HTML,記爲變量markup。而mountComponentIntoNode最終調用的是_mountImageIntoNode,看下源碼:

核心代碼就是最後兩行。setInnerHTML是一個方法,將markup設置爲containerinnerHTML屬性,這樣就完成了DOM的插入。precacheNode方法是將處理好的組件對象存儲在緩存中,提升結構更新的速度。

React組件初始化和掛載的流程到這裏基本明朗了。在ReactDOM.render()的方法使用中,咱們會注意到該方法能夠掛載React組件,也能夠掛載字符串,也能夠掛載原生DOM。如今咱們已經知道,其實掛載就是利用innerHTML屬性,可是對於不一樣的元素結構,React是否也有不一樣的處理呢?

上文咱們提到,在組件掛載的倒數第二步,也就是執行_renderNewRootComponent方法時,咱們看到有一個名爲instantiateReactComponent的方法返回一個通過加工的對象。咱們看下instantiateReactComponent的源碼:

傳入的參數node就是ReactDOM.render方法的組件參數,輸入node和輸出instance能夠總結以下表:

node 實際參數 結果
null/false 建立ReactEmptyComponent組件
object && type === string 虛擬DOM 建立ReactDOMComponent組件
object && type !== string React組件 建立ReactCompositeComponent組件
string 字符串 建立ReactTextComponent組件
number 數字 建立ReactTextComponent組件

梳理一下流程:

  • 根據ReactDOM.render()傳入不一樣的參數,React內部會建立四大類封裝組件,記爲componentInstance
  • 然後將其做爲參數傳入mountComponentIntoNode方法中,由此得到組件對應的HTML,記爲變量markup
  • 將真實的DOM的屬性innerHTML設置爲markup,即完成了DOM插入。

那麼問題來了,在上述第二步是如何解析出HTML的呢?答案是在第一步封裝成四大類型組件的過程當中,賦予了封裝組件mountComponet方法, 執行該方法會觸發組件的生命週期,從而解析出HTML。

固然,這四大類組件咱們最經常使用的就是ReactCompositeComponent組件,也就是咱們常說的React組件,其內部具備完整的生命週期,也是React最關鍵的組件特性。關於詳細的組件類型與生命週期的部分,咱們在下一篇文章講解。

4.總結

用一張圖來梳理React組件從聲明到初始化再到掛載的流程: (點擊可查看大圖)

回顧:
《React源碼解析(二):組件的類型與生命週期》
《React源碼解析(三):詳解事務與更新隊列》
《React源碼解析(四):事件系統》 聯繫郵箱:ssssyoki@foxmail.com

相關文章
相關標籤/搜索