1 簡(jiǎn)介
React廣受好評(píng)的一個(gè)重要原因就是組件化開發(fā),一方面分模塊的方式便于協(xié)同開發(fā),降低耦合,后期維護(hù)也輕松;另一方面使得一次開發(fā),多處復(fù)用成為現(xiàn)實(shí),甚至可以直接復(fù)用開源React組件。開發(fā)完一個(gè)組件后,我們需要插入DOM中,一般使用如下代碼
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
經(jīng)過babel轉(zhuǎn)碼后為
ReactDOM.render(
React.createElement(
'h1', // type, DOM原生組件的type為string,React自定義組件type為Object
null, // config,會(huì)設(shè)置到ref,key,props中
'Hello, world!' // children,子組件.這兒為文本組件
),
document.getElementById('example')
)
那么React底層是怎么將組件插入DOM中的呢。本文來詳細(xì)分析它的前因后果。
2 ReactMount._renderSubtreeIntoContainer()
ReactDOM.render()實(shí)際調(diào)用ReactMount.render(),接著調(diào)用到ReactMount._renderSubtreeIntoContainer().
這個(gè)調(diào)用鏈比較簡(jiǎn)單,不分析了。下面重點(diǎn)分析_renderSubtreeIntoContainer(). 我們?nèi)コ糸_發(fā)調(diào)試階段的報(bào)錯(cuò)代碼(比如 “development” !== ‘production’)。
/**
* 將ReactElement插入DOM中,并返回ReactElement對(duì)應(yīng)的ReactComponent。
* ReactElement是React元素在內(nèi)存中的表示形式,可以理解為一個(gè)數(shù)據(jù)類,包含type,key,refs,props等成員變量
* ReactComponent是React元素的操作類,包含mountComponent(), updateComponent()等很多操作組件的方法
*
* @param {parentComponent} 父組件,對(duì)于第一次渲染,為null
* @param {nextElement} 要插入到DOM中的組件,對(duì)應(yīng)上面例子中的<h1>Hello, world!</h1>經(jīng)過babel轉(zhuǎn)譯后的元素
* @param {container} 要插入到的容器,對(duì)應(yīng)上面例子中的document.getElementById('example')獲取的DOM對(duì)象
* @param {callback} 第一次渲染為null
*
* @return {component} 返回ReactComponent,對(duì)于ReactDOM.render()調(diào)用,不用管返回值。
*/
_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
// 剛開始一段開發(fā)階段的報(bào)錯(cuò)代碼,省去
...
// 包裝ReactElement,將nextElement掛載到wrapper的props屬性下,這段代碼不是很關(guān)鍵
var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
// 獲取要插入到的容器的前一次的ReactComponent,這是為了做DOM diff
// 對(duì)于ReactDOM.render()調(diào)用,prevComponent為null
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
// 從prevComponent中獲取到prevElement這個(gè)數(shù)據(jù)對(duì)象。一定要搞清楚ReactElement和ReactComponent的作用,他們很關(guān)鍵
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props;
// DOM diff精髓,同一層級(jí)內(nèi),type和key不變時(shí),只用update就行。否則先unmount組件再mount組件
// 這是React為了避免遞歸太深,而做的DOM diff前提假設(shè)。它只對(duì)同一DOM層級(jí),type相同,key(如果有)相同的組件做DOM diff,否則不用比較,直接先unmount再mount。這個(gè)假設(shè)使得diff算法復(fù)雜度從O(n^3)降低為O(n).
// shouldUpdateReactComponent源碼請(qǐng)看后面的分析
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
// 只需要update,調(diào)用_updateRootComponent,然后直接return了
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, container, updatedCallback);
return publicInst;
} else {
// 不做update,直接先卸載再掛載。即unmountComponent,再mountComponent。mountComponent在后面代碼中進(jìn)行
ReactMount.unmountComponentAtNode(container);
}
}
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
var containerHasNonRootReactChild = hasNonRootReactChild(container);
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
// 初始化,渲染組件,然后插入到DOM中。_renderNewRootComponent很關(guān)鍵,后面詳細(xì)分析
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, parentComponent != null ? parentComponent._reactInternalInstance._processChildContext(parentComponent._reactInternalInstance._context) : emptyObject)._renderedComponent.getPublicInstance();
// render方法中帶入的回調(diào),ReactDOM.render()調(diào)用時(shí)一般不傳入
if (callback) {
callback.call(component);
}
return component;
},
shouldUpdateReactComponent()源碼如下:
function shouldUpdateReactComponent(prevElement, nextElement) {
// 前后兩次ReactElement中任何一個(gè)為null,則必須另一個(gè)為null才返回true。這種情況一般不會(huì)碰到
var prevEmpty = prevElement === null || prevElement === false;
var nextEmpty = nextElement === null || nextElement === false;
if (prevEmpty || nextEmpty) {
return prevEmpty === nextEmpty;
}
var prevType = typeof prevElement;
var nextType = typeof nextElement;
// React DOM diff算法
if (prevType === 'string' || prevType === 'number') {
// 如果前后兩次為數(shù)字或者字符,則認(rèn)為只需要update(處理文本元素),返回true
return (nextType === 'string' || nextType === 'number');
} else {
// 如果前后兩次為DOM元素或React元素,則必須type和key不變(key用于listView等組件,很多時(shí)候我們沒有設(shè)置key,故只需type相同)才update,否則先unmount再重新mount。返回false
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
3.ReactMount._renderNewRootComponent
_renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
// 初始化ReactComponent,根據(jù)ReactElement中不同的type字段,創(chuàng)建不同類型的組件對(duì)象,即ReactComponent
// 前一篇文章中已經(jīng)分析了。http://blog.csdn.net/u013510838/article/details/55669769
var componentInstance = instantiateReactComponent(nextElement);
// 處理batchedMountComponentIntoNode方法調(diào)用,將ReactComponent插入DOM中,后面詳細(xì)分析
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
return componentInstance;
},
batchedMountComponentIntoNode以transaction事務(wù)的形式調(diào)用mountComponentIntoNode,源碼分析如下。
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
var markerName;
// 一段log,可以不管
if (ReactFeatureFlags.logTopLevelRenders) {
var wrappedElement = wrapperInstance._currentElement.props;
var type = wrappedElement.type;
markerName = 'React mount: ' + (typeof type === 'string' ? type : type.displayName || type.name);
console.time(markerName);
}
// 調(diào)用對(duì)應(yīng)ReactComponent中的mountComponent方法來渲染組件,這個(gè)是React生命周期的重要方法。后面詳細(xì)分析。
// mountComponent返回React組件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多態(tài)
// 上面的<h1>Hello, world!</h1>, 對(duì)應(yīng)的是ReactDOMTextComponent,最終解析成的HTML為
// <h1 data-reactroot="">Hello, world!</h1>
var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context);
if (markerName) {
console.timeEnd(markerName);
}
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
// 將解析出來的HTML插入DOM中
ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
}
_mountImageIntoNode源碼如下
_mountImageIntoNode: function (markup, container, instance, shouldReuseMarkup, transaction) {
// 對(duì)于ReactDOM.render()調(diào)用,shouldReuseMarkup為false
if (shouldReuseMarkup) {
...
}
if (transaction.useCreateElement) {
// 清空container的子節(jié)點(diǎn),這個(gè)地方不明白為什么這么做
while (container.lastChild) {
container.removeChild(container.lastChild);
}
DOMLazyTree.insertTreeBefore(container, markup, null);
} else {
// 將markup這個(gè)HTML設(shè)置到container這個(gè)DOM元素的innerHTML屬性上,這樣就插入到了DOM中了
setInnerHTML(container, markup);
// 將instance這個(gè)ReactComponent渲染后的對(duì)象,即Virtual DOM,保存到container這個(gè)DOM元素的firstChild這個(gè)原生節(jié)點(diǎn)上。簡(jiǎn)單理解就是將Virtual DOM保存到內(nèi)存中,這樣可以大大提高交互效率
ReactDOMComponentTree.precacheNode(instance, container.firstChild);
}
}
4 總結(jié)
ReactDOM.render()是渲染React組件并插入到DOM中的入口方法,它的執(zhí)行流程大概為
1.React.createElement(),創(chuàng)建ReactElement對(duì)象。?他的重要的成員變量有type,key,ref,props。這個(gè)過程中會(huì)調(diào)用getInitialState(), 初始化state,只在掛載的時(shí)候才調(diào)用。后面update時(shí)不再調(diào)用了。
2.instantiateReactComponent(),根據(jù)ReactElement的type分別創(chuàng)建ReactDOMComponent, ReactCompositeComponent,ReactDOMTextComponent等對(duì)象
3.mountComponent(), 調(diào)用React生命周期方法解析組件,得到它的HTML。
4._mountImageIntoNode(), 將HTML插入到DOM父節(jié)點(diǎn)中,通過設(shè)置DOM父節(jié)點(diǎn)的innerHTML屬性。
5.緩存節(jié)點(diǎn)在React中的對(duì)應(yīng)對(duì)象,即Virtual DOM。