一、JSX如何生成element
這里是一段寫在render里的jsx代碼。
return (
<div className="cn">
<Header> Hello, This is React </Header>
<div>Start to learn right now!</div>
Right Reserve.
</div>
)
首先,它會經過babel編譯成React.createElement的表達式。
return (
React.createElement(
'div',
{ className: 'cn' },
React.createElement(
Header,
null,
'Hello, This is React'
),
React.createElement(
'div',
null,
'Start to learn right now!'
),
'Right Reserve'
)
)
createElement從它的名字就可以看出,這是用來生成element的。element在React里,其實就是組成虛擬DOM 樹的節點,它用來描述你想要在瀏覽器上看到什么。
它的參數有三個:
- type -> 標簽
- attributes -> 標簽屬性,沒有的話,可以為null
- children -> 標簽的子節點
這個React.createElement的表達式會在render函數被調用的時候執行,換句話說,當render函數被調用的時候,會返回一個element。
element其實就是一個對象,如下:
{
type: 'div',
props: {
className: 'cn',
children: [
{
type: function Header,
props: {
children: 'Hello, This is React'
}
},
{
type: 'div',
props: {
children: 'start to learn right now!'
}
},
'Right Reserve'
]
}
}
我們來觀察一下這個對象的children,現在有三種類型:
1、string
2、原生DOM節點
3、React Component - 自定義組件
除了這三種,還有兩種類型:
4、fale ,null, undefined,number
5、數組 - 使用map方法的時候
這里需要記住一個點:element不一定是Object類型。
二、element如何生成真實節點
順利得到element之后,我們再來看看React是如何把element轉化成真實DOM節點的。
首先,需要去初始化element,初始化的規則如下:
先判斷是否為Object類型,是的話,看它的type是否是原生DOM標簽,是的話,給它創建ReactDOMComponent的實例對象,其他同理。
這時候有的人可能會有所疑問:這些個ReactDOMComponent, ReactCompositeComponentWrapper怎么開發的時候都沒有見過?
其實這些都是React的私有類,React自己使用,不會暴露給用戶的。它們的常用方法有:mountComponent,updateComponent等。其中mountComponent 用於創建組件,而updateComponent用於用戶更新組件。而我們自定義組件的生命周期函數以及render函數都是在這些私有類的方法里被調用的。
既然這些私有類的方法那么重要我們就先來簡單了解一下吧~
ReactDOMComponent
首先是ReactMComponent的mountComponent方法,這個方法的作用是:將element轉成真實DOM節點,並且插入到相應的container里,然后返回markup(realDOM)。
由此可知ReactDOMComponent的mountComponent是element生成真實節點的關鍵。
下面看個栗子它是怎么做到的吧。
假設有這樣一個type類型是原生DOM的element:
{
type: 'div',
props: {
className: 'cn',
children: 'Hello world',
}
}
簡單mountComponent的實現:
mountComponent(container) {
const domElement = document.createElement(this._currentElement.type);
const textNode = document.createTextNode(this._currentElement.props.children);
domElement.appendChild(textNode);
container.appendChild(domElement);
return domElement;
}
其實實現的過程很簡單,就是根據type生成domElement,再將子節點append進來返回。當然,真實的mountComponent沒有那么簡單,感興趣的可以自己去看源碼啦。
這里需要記住的一個點是:這個類的mountComponent方法會自己操作瀏覽器DOM元素。
講完ReactDOMComponent,再來看看ReactCompositeComponentWrapper。
ReactCompositeComponentWrapper
這個類的mountComponent方法作用是:實例化自定義組件,最后是通過遞歸調用到ReactDOMComponent的mountComponent方法來得到真實DOM。
注意:也就是說他自己是不直接生成DOM節點的。
那這個遞歸是一個怎樣的過程呢?我們通過首次渲染來看下。
首次渲染
假設我們有一個Example的組件,它返回
首次渲染的過程如下:
首先從React.render開始,由於我們剛剛說,render函數被調用的時候會返回一個element,所以此時返回給我們的element是:
{
type: function Example,
props: {
children: null
}
}
由於這個type是一個自定義組件類,此時要初始化的類是ReactCompositeComponentWrapper,接着調用它的mountComponent方法。這里面會做四件事情,詳情可以看上圖。其中,第二步的render的得到的element為:
{
type: 'div',
props: {
children: 'Hello World'
}
}
由於這個type是一個原生DOM標簽,此時要初始化的類是ReactDOMComponent。接下來它的mountComponent方法就可以幫我們生成對應的DOM節點放在瀏覽器里啦。
這時候有人可能會有疑問,如果第二步render出來的element 類型也是自定義組件呢?
這時候它就會去調用ReactCompositeComponentWrapper的mountComponent方法,從而形成了一個遞歸。不管你的自定義組件嵌套多少層,最后總會生成原生dom類型的element,所以最后一定能調用到ReactDOMComponent的mountComponent方法。
感興趣的可以自己在打斷點看下這個遞歸的過程。
由我打的斷點圖可以看出在ReactCompositeComponent的mountComponent被調用多次之后,最后調用到了ReactDOMComponent的mountComponent方法。
還有一個問題:前面我們說自定義組件的生命周期跟render函數都是在私有類的方法里被調用的,現在只看到render函數被調用了,那么首次渲染時候生命周期函數 componentWillMount 跟 componentDidMount 在哪被調用呢?
由圖可知,在第一步得到instance對象之后,就會去看instance.componentWillMount是否有被定義,有的話調用,而在整個渲染過程結束之后調用componentDidMount。
以上,就是渲染原理的部分,讓我們來總結以下:
JSX代碼經過babel編譯之后變成React.createElement的表達式,這個表達式在render函數被調用的時候執行生成一個element。
在首次渲染的時候,先去按照規則初始化element,接着ReactComponentComponentWrapper通過遞歸,最終調用ReactDOMComponent的mountComponent方法來幫助生成真實DOM節點。