-
程式假設有如下 jsx
class Form extends React.Component {
constructor() {
super();
}
render() {
return (
<form>
<input type="text"/>
</form>
);
}
}
ReactDOM.render( (
<div className="test">
<span onClick={function(){}}>CLICK ME</span>
<Form/>
</div>
), document.getElementById(`main`))
-
拿 ReactDOM render 的部分(
<div className="test">...</div>
)為例,用 babel 把 jsx 轉成 js 後得到如下程式碼:
React.createElement( `div`, {
className: `test`
},
React.createElement( `span`,
{ onClick: function(){} },
`CLICK ME`
),
React.createElement(Form, null)
)
-
這裡看下 API: React.createElement(component, props, …children)。它生成一個 js 的物件,這個物件是用來代表一個真實的 dom node,這個 js 的物件就是我們俗稱的虛擬dom。(虛擬 dom 的意思是用 js 物件結構模擬出 html 中 dom 結構,批量的增刪改查先直接操作 js 物件,最後更新到真正的 dom 樹上。因為直接操作 js 物件的速度要比操作 dom 的那些 api 要快。) 譬如根元素
<div className="test"></div>
生成對應出來的虛擬 dom 是:
{
type: `div`,
props: {
className: `test`,
children: []
}
}
除了一些 dom 相關的屬性,虛擬 dom 物件還包括些 React 自身需要的屬性,譬如:ref,key。最終示例中的 jsx 生成出來的虛擬 dom,大致如下:
{
type: `div`,
props: {
className: `xxx`,
children: [ {
type: `span`,
props: {
children: [ `CLICK ME` ]
},
ref:
key:
}, {
type: Form,
props: {
children: []
},
ref:
key:
} ] | Element
}
ref: `xxx`,
key: `xxx`
}
-
有了虛擬 dom,接下來的工作就是把這個虛擬 dom 樹真正渲染成一個 dom 樹。React 的做法是針對不同的 type 構造相應的渲染物件,渲染物件提供一個 mountComponent 方法(負責把對應的某個虛擬 dom 的節點生成成具體的 dom node),然後迴圈迭代整個 vdom tree 生成一個完整的 dom node tree,最終插入容器節點。檢視原始碼你會發現如下程式碼:
// vdom 是第3步生成出來的虛擬 dom 物件
var renderedComponent = instantiateReactComponent( vdom );
// dom node
var markup = renderedComponent.mountComponent();
// 把生成的 dom node 插入到容器 node 裡面,真正在頁面上顯示出來
// 下面是虛擬碼,React 的 dom 操作封裝在 DOMLazyTree 裡面
containerNode.appendChild( markup );
instantiateReactComponent 傳入的是虛擬 dom 節點,這個方法做的就是根據不同的 type 呼叫如下方法生成渲染物件:
// 如果節點是字串或者數字
return ReactHostComponent.createInstanceForText( vdom(string|number) );
// 如果節點是宿主內建節點,譬如瀏覽器的 html 的節點
return ReactHostComponent.createInternalComponent( vdom );
// 如果是 React component 節點
return new ReactCompositeComponentWrapper( vdom );
ReactHostComponent.createXXX 也只是一層抽象,不是最終的的渲染物件,這層抽象遮蔽了宿主。譬如手機端(React native)和瀏覽器中同樣呼叫 ReactHostComponent.createInternalComponent( vdom ); 他生成的最終的渲染物件是不同的,我們當前只討論瀏覽器環境。字串和數字沒有什麼懸念,在這裡我們就不深入探討了,再進一步看,div 等 html 的原生 dom 節點對應的渲染物件是 ReactDOMComponent 的例項。如何把 { type:`div`, … } 生成一個 dom node 就在這個類(的 mountComponent 方法)裡面。(對如何生成 div、span、input、select 等 dom node 感興趣的可以去探究 ReactDOMComponent,這裡不做具體的討論,本文只是想總結下 React 整個渲染過程。下面只給出一個最簡示例程式碼:)
class ReactDOMComponent {
constructor( vdom ) {
this._currentElement = vdom;
}
mountComponent() {
var result;
var props = this._currentElement.props;
if ( this._currentElement.type === `div` ) {
result = document.createElement( `div` );
for(var key in props ) {
result.setAttribute( key, props[ key ] );
}
} else {
// 其他型別
}
// 迭代子節點
props.children.forEach( child=>{
var childRenderedComponent = = instantiateReactComponent( child );
var childMarkup = childRenderedComponent.mountComponent();
result.appendChild( childMarkup );
} )
return result;
}
}
我們再看下 React component 的渲染物件 ReactCompositeComponentWrapper(主要實現在 ReactCompositeComponent 裡面,ReactCompositeComponentWrapper 只是一個防止迴圈引用的 wrapper)
// 以下是虛擬碼
class ReactCompositeComponent {
_currentElement: vdom,
_rootNodeID: 0,
_compositeType:
_instance:
_hostParent:
_hostContainerInfo:
// See ReactUpdateQueue
_updateBatchNumber:
_pendingElement:
_pendingStateQueue:
_pendingReplaceState:
_pendingForceUpdate:
_renderedNodeType:
_renderedComponent:
_context:
_mountOrder:
_topLevelWrapper:
// See ReactUpdates and ReactUpdateQueue.
_pendingCallbacks:
// ComponentWillUnmount shall only be called once
_calledComponentWillUnmount:
// render to dom node
mountComponent( transaction, hostParent, hostContainerInfo, context ) {
// ---------- 初始化 React.Component --------------
var Component = this._currentElement.type;
var publicProps = this._currentElement.props;
/*
React.Component 元件有2種:
new Component(publicProps, publicContext, updateQueue);
new StatelessComponent(Component);
對應的 compositeType 有三種
this._compositeType = StatelessFunctional | PureClass | ImpureClass,
元件種類和 compositeType 在原始碼中都有區分,但是這裡為了簡單,只示例最常用的一種元件的程式碼
*/
var inst = new Component(publicProps, publicContext, updateQueue);
inst.props = publicProps;
inst.context = publicContext;
inst.refs = emptyObject;
inst.updater = updateQueue;
// 渲染物件儲存元件物件
this._instance = inst;
// 通過 map 又把元件物件和渲染物件聯絡起來
ReactInstanceMap.set(inst, this);
/*
ReactInstanceMap: {
-----------------------------------------------
| |
v |
React.Component: ReactCompositeComponentWrapper { |
_instance: <-------------------------------------
}
}
這樣雙方都在需要對方的時候可以獲得彼此的引用
*/
// ---------- 生成 React.Component 的 dom --------------
// 元件生命週期函式 componentWillMount 被呼叫
inst.componentWillMount();
// 呼叫 render 方法返回元件的虛擬 dom
var renderedElement = inst.render();
// save nodeType
var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
// 根據元件的虛擬 dom 生成渲染物件
var child = instantiateReactComponent(renderedElement)
this._renderedComponent = child;
// 生成真正的 dom node
// 其實原始碼中的真正程式碼應該是 var markup = ReactReconciler.mountComponent( child, ... ),
// 這裡為了簡化說明,先不深究 ReactReconciler.mountComponent 還做了點什麼
var markup = child.mountComponent();
// 把元件生命週期函式 componentDidMount 註冊到回撥函式中,當整個 dom node tree 被新增到容器節點後觸發。
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
return markup;
}
}
// static member
ReactCompositeComponentWrapper._instantiateReactComponent = instantiateReactComponent
-
最終的過程是:
<div className="test">
<span onClick={this.click}>CLICK ME</span>
<Form/>
</div>
|
babel and React.createElement
|
v
{
type: `div`,
props: {
className: `xxx`,
children: [ {
type: `span`,
props: {
children:
},
ref:
key:
}, {
type: Form,
props: {
children:
},
ref:
key:
} ] | Element
}
ref: `xxx`,
key: `xxx`
}
|var domNode = new ReactDOMComponent( vdom ).mountComponent();
|
v
domNode = {
ReactDOMComponent -> <div/>
props: {
children: [
ReactDOMComponent -> <span/>
ReactCompositeComponentWrapper.render() -> vdom -> instantiateReactComponent(vdom) -> <form><input/></from>
]
}
}
|
|
vcontainerDomNode.appendChild( domNode );
以上是 React 渲染 dom 的一個基本流程,下一篇計劃總結下更新 dom 的流程,即 setState 後發生了什麼。