react: v15.0.0
本文講 元件如何編譯 以及 ReactDOM.render 的渲染過程。
babel 的編譯
babel 將 React JSX 編譯成 JavaScript.
在 babel 官網寫一段 JSX 程式碼編譯結果如圖:
每個標籤的建立都呼叫了 React.createElement.
原始碼中的兩種資料結構
貫穿原始碼,常見的兩種資料結構,有助於快速閱讀原始碼。
ReactElement
結構如下:
{
$$typeof // ReactElement識別符號
type // 元件
key
ref
props // 元件屬性和children
}
是 React.createElement 的返回值。
ReactComponent
ReactComponent 這個名字有點奇怪。
結構如下:
{
_currentElement // ReactElement
...
// 原型鏈上的方法
mountComponent, // 元件初次載入呼叫
updateComponent, // 元件更新呼叫
unmountComponent, // 元件解除安裝呼叫
}
是 ReactCompositeComponent 的 instance 型別。其餘三種建構函式 ReactDOMComponent、ReactDOMTextComponent、ReactEmptyComponent 的例項結構與其相似。
React.createElement
React.createElement 實際執行的是 ReactElement.createElement。
ReactElement.createElement 接收三個引數, 返回 ReactElement 結構。
- type: string | Component
- config: 標籤上的屬性
- ...children: children元素集合
重點關注 type 和 props。
然後看 ReactElement 方法,只是做了賦值動作。
綜上,我們寫的程式碼編譯後是這樣的:
class C extends React.Component {
render() {
return {
type: "div",
props: {
children: this.props.value,
},
};
}
}
class App extends React.Component {
render() {
return {
type: "div",
props: {
children: [
{
type: "span",
props: {
children: "aaapppppp",
},
},
"123",
{
type: C,
props: {
value: "ccc",
},
},
]
},
};
}
}
ReactDOM.render(
{
type: App,
props: {},
},
document.getElementById("root")
);
ReactDOM.render
先來看下 ReactDOM.render 原始碼的執行過程
instantiateReactComponent
在 _renderNewRootComponent 方法中,呼叫了 instantiateReactComponent,生成了的例項結構類似於 ReactComponent。
instantiateReactComponent 的引數是 node,node 的其中一種格式就是 ReactElement。
根據 node & node.type 的型別,會執行不同的方法生成例項
- ReactCompositeComponent
- ReactDOMComponent
- ReactDOMTextComponent
- ReactEmptyComponent
簡化如下
var instantiateReactComponent = function (node) {
if (node === null || node === false) {
return new ReactEmptyComponent(node);
} else if (typeof node === 'object') {
if (node.type === 'string') {
return new ReactDOMComponent(node);
} else {
return new ReactCompositeComponent(node);
}
} else if (typeof node === 'string' || typeof node === 'number') {
return new ReactDOMTextComponent(node);
}
}
通過四種方式例項化後的物件基本相似
var instance = {
_currentElement: node,
_rootNodeID: null,
...
}
instance.__proto__ = {
mountComponent,
updateComponent,
unmountComponent,
}
四種 mountComponent 簡化如下
ReactCompositeComponent
mountComponent: function () {
// 建立當前元件的例項
this._instance = new this._currentElement.type();
// 呼叫元件的 render 方法,得到元件的 renderedElement
renderedElement = this._instance.render();
// 呼叫 instantiateReactComponent, 得到 renderedElement 的例項化 ReactComponent
this._renderedComponent = instantiateReactComponent(renderedElement);
// 呼叫 ReactComponent.mountComponent
return this._renderedComponent.mountComponent();
}
ReactDOMComponent
react 原始碼中,插入 container 前使用 ownerDocument、DOMLazyTree 建立和存放節點,此處為了方便理解,使用 document.createElement 模擬。
mountComponent: function () {
var { type, props } = this._currentElement;
// 建立dom 原始碼中使用 ownerDocument
var element = document.createElement(type);
// 遞迴children (原始碼中使用 DOMLazyTree 存放 並返回)
if (props.children) {
var childrenMarkups = props.children.map(function (node) {
var instance = instantiateReactComponent(node);
return instance.mountComponent();
})
element.appendChild(childrenMarkups)
}
return element;
}
ReactDOMTextComponent
mountComponent: function () {
return this._currentElement;
}
ReactEmptyComponent
mountComponent: function () {
return null;
}
ReactDOM.render 簡化
簡化如下:
ReactDOM.render = function (nextElement, container) {
// 新增殼子
var nextWrappedElement = ReactElement(
TopLevelWrapper,
null,
null,
null,
null,
null,
nextElement
);
// 例項化 ReactElement
var componentInstance = instantiateReactComponent(nextElement);
// 遞迴生成html
var markup = componentInstance.mountComponent;
// 插入真實dom
container.innerHTML = markup;
}
總結
- babel 將 JSX 語法編譯成 React.createElement 形式。
- 原始碼中用到了兩個重要的資料結構
- ReactElement
- ReactComponent
- React.createElement 將我們寫的元件處理成 ReactElement 結構。
- ReactDOM.render 傳入 ReactElement 和 container, 渲染流程如下
- 在 ReactElement 外套一層,生成新的 ReactElement
- 例項化 ReactElement:var instance = instantiateReactComponent(ReactElement)
- 遞迴生成 markup:var markup = instance.mountComponent()
- 將 markup 插入 container:container.innerHTML = markup