React原始碼解析(一):元件的實現與掛載

ssssyoki發表於2017-10-18

當我們能夠熟練運用React進行前端開發時,不免會對React內部機制產生濃厚的興趣。元件是什麼?是真的DOM嗎?生命週期函式的執行依據又是什麼呢?

本篇,我們先來研究React元件的實現與掛載。

1.元件是什麼

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

React原始碼解析(一):元件的實現與掛載

上述程式碼寫完後,我們就得到了<A />這個元件,那麼我們接下來先弄清楚<A />是什麼。用console.log列印出來:

React原始碼解析(一):元件的實現與掛載

可以看出,<A />其實是js物件而不是真實的DOM,注意此時props是空物件。接下來,我們列印<A><div>這是A元件</div></A>,看看控制檯會輸出什麼:

React原始碼解析(一):元件的實現與掛載

我們看到,props發生了變化,由於<A />元件中巢狀了一個divdiv中又巢狀了文字,所以在描述<A />物件的props中增加了children屬性,其值為描述div的js物件。同理,如果我們進行多層的元件巢狀,其實就是在父物件的props中增加children欄位及對應的描述值,也就是js物件的多層巢狀。

以上描述是基於ES6的React開發模式,其實在ES5中通過React.createClass({})方法建立的元件,與ES6中是完全一樣的,同樣可以通過控制檯列印輸出元件結果進行驗證,此處不再贅述。

那麼形如HTML標籤實際上卻是物件的React元件是如何構成的呢?

因為我們的元件宣告基於ReactComponent,所以首先我們開啟React.js,可以看到如下程式碼:

React原始碼解析(一):元件的實現與掛載

我們在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:

React原始碼解析(一):元件的實現與掛載

上述程式碼是再熟悉不過的建構函式,想必大家已經滾瓜爛熟了。同時我們也注意到setState是定義在原型上具有兩個引數的方法,具體原理我們將在React更新機制的篇章講解。

上述程式碼表明,我們在最開始宣告的元件A,其實是繼承ReactComponent類的子類,它的原型具有setState等方法。這樣元件A已經有了最基本的雛形。

小結

React原始碼解析(一):元件的實現與掛載

2.元件的初始化

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

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

React原始碼解析(一):元件的實現與掛載

其中_Component是物件ReactComponent_inherit方法是extends關鍵字的函式實現,這些都是ES6相關內容,我們暫時不管。關鍵在於我們發現render方法實際上是呼叫了React.createElement方法(實際是ReactElement方法)。然後我們開啟ReactElement.js:

React原始碼解析(一):元件的實現與掛載

看到這裡我們發現,其實每一個元件物件都是通過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物件。

小結

對之前的思維導圖進行補充:

React原始碼解析(一):元件的實現與掛載

3.元件的掛載

我們知道可以通過ReactDOM.render(component,mountNode)的形式對自定義元件/原生DOM/字串進行掛載,

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

ReactDOM.render實際呼叫了內部的ReactMount.render,進而執行ReactMount._renderSubtreeIntoContainer。從字面意思上就可以看出是將"子DOM"插入容器的邏輯,我們看下原始碼實現:

React原始碼解析(一):元件的實現與掛載

這段程式碼非常重要,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的原始碼:

React原始碼解析(一):元件的實現與掛載

分析一下流程:

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

React原始碼解析(一):元件的實現與掛載

核心程式碼就是最後兩行。setInnerHTML是一個方法,將markup設定為containerinnerHTML屬性,這樣就完成了DOM的插入。precacheNode方法是將處理好的元件物件儲存在快取中,提高結構更新的速度。

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

上文我們提到,在元件掛載的倒數第二步,也就是執行_renderNewRootComponent方法時,我們看到有一個名為instantiateReactComponent的方法返回一個經過加工的物件。我們看下instantiateReactComponent的原始碼:

React原始碼解析(一):元件的實現與掛載

傳入的引數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原始碼解析(三):詳解事務與更新佇列》
《React原始碼解析(四):事件系統》
聯絡郵箱:ssssyoki@foxmail.com

相關文章