當我們能夠熟練運用React進行前端開發時,不免會對React內部機制產生濃厚的興趣。元件是什麼?是真的DOM嗎?生命週期函式的執行依據又是什麼呢?
本篇,我們先來研究React元件的實現與掛載。
1.元件是什麼
首先編寫一個最簡單的元件:
上述程式碼寫完後,我們就得到了<A />
這個元件,那麼我們接下來先弄清楚<A />
是什麼。用console.log
列印出來:
可以看出,<A />
其實是js物件而不是真實的DOM,注意此時props
是空物件。接下來,我們列印<A><div>這是A元件</div></A>
,看看控制檯會輸出什麼:
我們看到,props
發生了變化,由於<A />
元件中巢狀了一個div
,div
中又巢狀了文字,所以在描述<A />
物件的props
中增加了children
屬性,其值為描述div
的js物件。同理,如果我們進行多層的元件巢狀,其實就是在父物件的props
中增加children
欄位及對應的描述值,也就是js物件的多層巢狀。
以上描述是基於ES6的React開發模式,其實在ES5中通過React.createClass({})
方法建立的元件,與ES6中是完全一樣的,同樣可以通過控制檯列印輸出元件結果進行驗證,此處不再贅述。
那麼形如HTML標籤實際上卻是物件的React元件是如何構成的呢?
因為我們的元件宣告基於React
和Component
,所以首先我們開啟React.js
,可以看到如下程式碼:
我們在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已經有了最基本的雛形。
小結
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
;如果有即prevComponent
為true
,執行更新流程,即呼叫_updateRootComponent
方法。若不存在,則解除安裝。(呼叫unmountComponentAtNode
方法) - line 24:不管是更新還是解除安裝,最終都要掛載到真實的DOM上。看下
._renderNewRootComponent
的原始碼:
分析一下流程:
- 第3行出現了
instantiateReactComponent
包裝方法,這個我們後面再說。 - 第5行中
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
。 - 將真實的DOM的屬性
innerHTML
設定為markup
,即完成了DOM插入。
那麼問題來了,在上述第二步是如何解析出HTML的呢?答案是在第一步封裝成四大型別元件的過程中,賦予了封裝元件mountComponet
方法, 執行該方法會觸發元件的生命週期,從而解析出HTML。
當然,這四大類元件我們最常用的就是ReactCompositeComponent
元件,也就是我們常說的React元件,其內部具有完整的生命週期,也是React最關鍵的元件特性。關於詳細的元件型別與生命週期的部分,我們在下一篇文章講解。
4.總結
用一張圖來梳理React元件從宣告到初始化再到掛載的流程: (點選可檢視大圖)
回顧:
《React原始碼解析(二):元件的型別與生命週期》
《React原始碼解析(三):詳解事務與更新佇列》
《React原始碼解析(四):事件系統》
聯絡郵箱:ssssyoki@foxmail.com