React源码解析——组件的挂载
我们知道React应用的常见结构是由JSX语法构成各种各样的组件,然后通过render方法挂载到相关的父组件上构成一颗组件树,最终将整个组件树挂载到真实DOM上。本文分析该过程的实现原理。
组件初始化
既然要挂载组件那必然得先创造一个组件,以下是一个最简单的例子:
import React from 'react';
class A extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return <div>hello<div>
}
}
可以看到我们的组件需要继承自React的Component类,下面就看一下这部分的源码。
var React = {
// ...
Component: ReactComponent,
createElement: createElement
}
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
// ...
}
ReactComponent.prototype.setState = function(partialState, callback) {
// ...
}
ReactComponent.prototype.forceUpdate = function(callback) {
// ...
}
可以发现我们从React.Component中继承到了React组件的通用属性和方法。那么其结构呢?结构是由JSX提供的,经过babel转义后会发现JSX会转化为React的createElement方法来创建。源码如下:
ReactElement.createElement = function(type, config, children) {
var propName;
// Reserved names are extracted
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
if (config != null) {
ref = config.ref === undefined ? null : config.ref;
key = config.key === undefined ? null : '' + config.key;
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (config.hasOwnProperty(propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (typeof props[propName] === 'undefined') {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
);
};
代码比较简单,主要是处理参数,将children放入props中。然后通过ReactElement方法创建出ReactElement类型的对象。源码如下:
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
return element;
};
到这里我们就可以发现,每一个组件其实就是一个ReactElement类型的JS对象了,也就是常说的虚拟DOM。
组件的挂载
现在我们已经有了组件对象,就可以通过render方法去挂载了。下面进入到render方法的源码:
var TopLevelWrapper = function () {
this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.render = function () {
// this.props is actually a ReactElement
return this.props;
};
var ReactMount = {
// ...
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},
//若组件未挂载,那么将react组件挂载到DOM上,若组件已被挂载,那么将执行组件更新机制
_renderSubtreeIntoContainer:function (parentComponent, nextElement, container, callback) {
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
//将组件添加到前一级wrapper的props属性下
var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
var nextContext;
//如果父组件存在,则存储在nextContext中,否则存储空对象
if (parentComponent) {
var parentInst = ReactInstanceMap.get(parentComponent);
nextContext = parentInst._processChildContext(parentInst._context);
} else {
nextContext = emptyObject;
}
//判断容器下是否已经存在组件,对于ReactDOM.render()来说为空
var prevComponent = getTopLevelWrapperInContainer(container);
//假设该容器已存在组件且类型和索引相同时,依据Diff算法只对当前组件进行更新,否则进行卸载
if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props;
//组件更新机制在生命周期部分进行解析
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback = callback && function () {
callback.call(publicInst);
};
//更新后返回
ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container,updatedCallback);
return publicInst;
} else {
//卸载
ReactMount.unmountComponentAtNode(container);
}
}
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);
var containerHasNonRootReactChild = hasNonRootReactChild(container);
//经过上述流程,确认容器为空或容器内的组件已卸载,那么调用_renderNewRootComponent插入DOM
var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
//此处说明ReactDOM.render可以传三个参数,包括回调函数
if (callback) {
callback.call(component);
}
return component;
}
首先是调用了_renderSubtreeIntoContainer方法,该方法通过ReactElement创建一个虚拟DOM,再调用 _renderNewRootComponent,代码如下:
_renderNewRootComponent: function(
nextElement,
container,
shouldReuseMarkup,
context
) {
var componentInstance = instantiateReactComponent(nextElement, null);
....
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
reactRootID,
container,
shouldReuseMarkup,
context
);
....
return componentInstance;
}
首先通过instantiateReactComponent方法创建虚拟DOM对应的ReactComponent对象,根据nextElement参数的不同会创建四大类组件:
- 为空时创建ReactEmptyComponent组件
- 为React组件时创建ReactCompositeComponent组件
- 为字符串或数字时创建ReactTextComponent组件
- 为DOM时创建ReactDOMComponent组件
生命周期相关的方法就在这些组件中初始化。
再通过batchedUpdates方法去调用batchedMountComponentIntoNode,该方法又以事务的形式去调用mountComponentIntoNode,源码如下:
function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
var markerName;
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);
}
// 调用对应ReactComponent中的mountComponent方法来渲染组件
// mountComponent返回React组件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多态
// 上面的<h1>Hello, world!</h1>, 对应的是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);
}
这一步会根据以上四类组件的不同分别调用其mountComponent方法去渲染组件为HTML,最后通过_mountImageIntoNode将这个HTML设置到container这个DOM元素的innerHTML上就完成了组件的挂载。
总结
组件的挂载流程按关键节点大致可分为如下几步:
- 通过React.createElement()创建ReactElement对象,即虚拟DOM
- 通过instantiateReactComponent()根据虚拟DOM的类型分别创建不同类型的ReactComponent对象
- 调用不同类型对象的mountComponent()得到HTML
- _mountImageIntoNode()将HTML通过innerHTML插入到真实DOM中
-- EOF --