當咱們可以熟練運用React進行前端開發時,難免會對React內部機制產生濃厚的興趣。組件是什麼?是真的DOM嗎?生命週期函數的執行依據又是什麼呢?前端
本篇,咱們先來研究React組件的實現與掛載。node
首先編寫一個最簡單的組件:react
上述代碼寫完後,咱們就獲得了<A />
這個組件,那麼咱們接下來先弄清楚<A />
是什麼。用console.log
打印出來:git
能夠看出,<A />
實際上是js對象而不是真實的DOM,注意此時props
是空對象。接下來,咱們打印<A><div>這是A組件</div></A>
,看看控制檯會輸出什麼:github
咱們看到,props
發生了變化,因爲<A />
組件中嵌套了一個div
,div
中又嵌套了文字,因此在描述<A />
對象的props
中增長了children
屬性,其值爲描述div
的js對象。同理,若是咱們進行多層的組件嵌套,其實就是在父對象的props
中增長children
字段及對應的描述值,也就是js對象的多層嵌套。typescript
以上描述是基於ES6的React開發模式,其實在ES5中經過React.createClass({})
方法建立的組件,與ES6中是徹底同樣的,一樣能夠經過控制檯打印輸出組件結果進行驗證,此處再也不贅述。緩存
那麼形如HTML標籤實際上倒是對象的React組件是如何構成的呢?babel
由於咱們的組件聲明基於React
和Component
,因此首先咱們打開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
,這二者是否存在區別呢?答案是否認的。由於Component
是React.Component
的引用。也就是說Component === React.Component
,在實際項目中寫哪一個均可以。沿着ReactComponent
的線索,咱們打開node_modules/react/lib/ReactComponent.js
:
上述代碼是再熟悉不過的構造函數,想必你們已經倒背如流了。同時咱們也注意到setState
是定義在原型上具備兩個參數的方法,具體原理咱們將在React更新機制的篇章講解。
上述代碼代表,咱們在最開始聲明的組件A,實際上是繼承ReactComponent
類的子類,它的原型具備setState
等方法。這樣組件A已經有了最基本的雛形。
聲明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對象。
對以前的思惟導圖進行補充:
咱們知道能夠經過ReactDOM.render(component,mountNode)
的形式對自定義組件/原生DOM/字符串進行掛載,
那麼掛載的過程又是如何實現的呢?
ReactDOM.render
實際調用了內部的ReactMount.render
,進而執行ReactMount._renderSubtreeIntoContainer
。從字面意思上就能夠看出是將"子DOM"插入容器的邏輯,咱們看下源碼實現:
這段代碼很是重要,render
函數的功能所有再在此(可點擊圖片大圖)。
咱們先來解析傳入_renderSubtreeIntoContainer
的參數:
參數 | 功能 |
---|---|
parentComponent |
當前組件的父組件,第一次渲染時爲null |
nextElement |
要插入DOM中的組件,如helloWorld |
container |
要插入的容器,如document.getElementById('root') |
callback |
完成後的回調函數 |
這幾個參數的功能很好理解,接下來咱們逐行進行邏輯分析:
props
屬性下。(本文開頭已說明父子嵌套關係由props
提供)getTopLevelWrapperInContainer
方法判斷當前容器下是否存在組件,記爲prevComponent
;若是有即prevComponent
爲true
,執行更新流程,即調用_updateRootComponent
方法。若不存在,則卸載。(調用unmountComponentAtNode
方法)._renderNewRootComponent
的源碼:分析一下流程:
instantiateReactComponent
包裝方法,這個咱們後面再說。batchedMountComponentIntoNode
以事務的形式調用mountComponentIntoNode
(事務將專門拿出一篇文章來解析),該方法返回組件對應的HTML,記爲變量markup
。而mountComponentIntoNode
最終調用的是_mountImageIntoNode
,看下源碼:核心代碼就是最後兩行。setInnerHTML
是一個方法,將markup
設置爲container
的innerHTML
屬性,這樣就完成了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
。innerHTML
設置爲markup
,即完成了DOM插入。那麼問題來了,在上述第二步是如何解析出HTML的呢?答案是在第一步封裝成四大類型組件的過程當中,賦予了封裝組件mountComponet
方法, 執行該方法會觸發組件的生命週期,從而解析出HTML。
固然,這四大類組件咱們最經常使用的就是ReactCompositeComponent
組件,也就是咱們常說的React組件,其內部具備完整的生命週期,也是React最關鍵的組件特性。關於詳細的組件類型與生命週期的部分,咱們在下一篇文章講解。
用一張圖來梳理React組件從聲明到初始化再到掛載的流程: (點擊可查看大圖)
回顧:
《React源碼解析(二):組件的類型與生命週期》
《React源碼解析(三):詳解事務與更新隊列》
《React源碼解析(四):事件系統》 聯繫郵箱:ssssyoki@foxmail.com