本文研究的版本为reactv0.8.0
在v0.8.0版本中,react组件初始挂载实现是相对简单的。总体可以划分为两部分来阐述:组件实例化和(真正的)组件挂载。因为先理解组件实例化,再理解组件挂载会比较好。所以,我先介绍组件实例化,后介绍(真正的)组件挂载流程。
组件实例化
什么是react组件?
在搞懂什么是react组件之前,我们不妨先了解一下“组件”的定义。显然,“组件”这个概念并不是软件编程界所独有的,它应该是来源于工程学。
卡耐基梅隆大学给“组件”下过这样的定义:一个不透明的功能实体,能够被第三方组装,且符合一个构件模型。
计算机百科全书是这样说的:是软件系统中具有相对独立功能、接口由契约指定、和语境有明显依赖关系、可独立部署、可组装的软件实体。
软件构件著作中如是定义:是一个组装单元,它具有约定式规范的接口,以及明确的依赖环境。构建可以被独立的部署,由第三方组装。
不同的上下文中,“组件”的定义是略有不同的。但是,共同点是有的。“功能独立,向外提供接口,可组装/组合”就是组件定义的基本要素。我们拿这三点对照一下后,会发现react组件是符合这三个基本要素的。
1.react组件是可组合的。例如我们会有这样的应用代码:
const A_Component = React.createClass({
render(){
return (
)
}
})
这种示例下,我们可以清晰地看到A_Component是由B_Component和C_Component组合而成的。而A_Component组件又可以参与别的组件的组合。
2.react组件有向外提供接口吗?显然,props就是react组件向外界提供的接口。
3.react组件功能独立吗?是的,独立。props完全可以没有的,react组件可以靠它内部state来驱动自己,保持功能的独立。
从最后的实现结果来看,react组件是符合这三个组件定义的基本要素的。那么,回归到“react”这个语景中,什么是“react”组件呢?
个人理解是这样的:
从历史追溯的角度看,“react组件”算是jquery+handlebar时代模板的进化产物。
从软件管理的角度看,“react组件”是“分而治之”和“高内聚低耦合”理念在前端落地的结果。
从使用react进行页面开发的角度看,“react组件”是构建页面的基本单元。
从代码实现的角度看,“react组件”是具有props,state等基本属性和render必要方法的一个类。
如果非要扯上点代码,那么我们可以说,由React.createClass()和React.DOM.XXX()返回的就是react组件。
以上是个人对react组件定义的理解。那么官方是怎么定义的呢?其实,官方也没有太严谨地,郑重其事地给它下定义,只是简短的一句话(见官网):
React components are small, reusable pieces of code that return a React element to be rendered to the page.
react组件的类别
在reactv0.8.0中,组件分为三个大的类别:
ReactCompositeComponent
ReactDOMComponent
ReactTextComponent
类是一个抽象的存在,那么我们不妨通过实例来具像化这三种类型组件的感知。于是,我们可以在控制台把它们都打印出来看看。
如果我们有ReactCompositeComponent组件如下:
const RCC = React.createClass({
render(){
return 'ReactCompositeComponent';
}
})
那么它的组件实例是这样的:
我们随便创建个ReactDOMComponent组件实例如下:
const DIV= React.DOM.div({},'ReactDOMComponent')
把它打印出来看看
最后,我们来看看ReactTextComponent组件实例长什么样:
const TEXT= new React.__internals.TextComponent('ReactTextComponent');
在react源码中,采用mixin模式实现了原型继承,并且很好地复用了代码。
下面看看实现
mixin/**
* Simply copies properties to the prototype.
*/
var mixInto = function(constructor, methodBag) {
var methodName;
for (methodName in methodBag) {
if (!methodBag.hasOwnProperty(methodName)) {
continue;
}
constructor.prototype[methodName] = methodBag[methodName];
}
};
从代码中,我们看到了mixInto通过遍历传递进来的methodBag,把它身上的方法逐个逐个地挂载在constructor的原型对象上来实现了原型继承和mixin模式的结合的。
所以,我们在探究react组件初始挂载过程中,定位某个方法的源码时,只要沿着原型链傻上找就好。好了,组件类型就讲到这里。下面,我们探索一下各种类型组件的具体实例化过程。
组件具体的实例化过程
ReactTextComponent构造函数是挂在React.__internals上的,只供内部使用,因此组件实例化也是由内部代码来完成的。这一节,我们主要是讨论ReactCompositeComponent和ReactDOMComponent的实例化过程。ReactTextComponent的实例化过程比较简单,我们放在最后讲。
因为源码实现的缘故,ReactCompositeComponent和ReactDOMComponent的实例化都是经过两次函数调用才完成的。而这么做的原因,值得我们深究。
ReactCompositeComponent的实例化过程
因为React.createClass方法引用的就是ReactCompositeComponent.createClass方法,所以,我们就直奔ReactCompositeComponent.js看看:
var ReactCompositeComponent = {
// ......
/**
* Creates a composite component class given a class specification.
*
* @param {object} spec Class specification (which must define `render`).
* @return {function} Component constructor function.
* @public
*/
createClass: function(spec) {
// 这里不妨这么写,能够帮助读者更清楚梳理各个“类”之间的关系
// 那就是:var Constructor = function ReactCompositeComponent(){}
var Constructor = function() {};
Constructor.prototype = new ReactCompositeComponentBase();
Constructor.prototype.constructor = Constructor;
mixSpecIntoComponent(Constructor, spec);
("production" !== process.env.NODE_ENV ? invariant(
Constructor.prototype.render,
'createClass(...): Class specification must implement a `render` method.'
) : invariant(Constructor.prototype.render));
if ("production" !== process.env.NODE_ENV) {
if (Constructor.prototype.componentShouldUpdate) {
console.warn(
(spec.displayName || 'A component') + ' has a method called ' +
'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
'The name is phrased as a question because the function is ' +
'expected to return a value.'
);
}
}
// Reduce time spent doing lookups by setting these on the prototype.
for (var methodName in ReactCompositeComponentInterface) {
if (!Constructor.prototype[methodName]) {
Constructor.prototype[methodName] = null;
}
}
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
};
ConvenienceConstructor.componentConstructor = Constructor;
ConvenienceConstructor.originalSpec = spec;
return ConvenienceConstructor;
},
// ......
}
可以看出,createClass方法的源码框架是这样的:
createClass: function(spec) {
var Constructor = function() {};
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
return instance;
};
return ConvenienceConstructor;
}
第一次调用是由用户完成的,像这样:
const SomeComponent = React.createClass({
render(){
return React.DOM.div({},'SomeComponent');
}
})
而对照上面的源码框架,我们可以知道,其实SomeComponent就是构造函数。再啰嗦点讲其实就是一个自定义类型。对的,组件本质上就是一个自定义类型。
然后,一般情况下,我们会用jsx的方式去消费SomeComponent:
const AnotherComponent = React.createClass({
render(){
return
}
})
我们大家都知道jsx
React.createClass方法调用之后返回的是一个构造函数,代表着一类组件,这个相信大家都有认识了。从源码看,上面的SomeComponent其实就是ConvenienceConstructor函数。现在我们聚焦一下ConvenienceConstructor函数的具体实现:
var ConvenienceConstructor = function(props, children) {
var instance = new Constructor();
instance.construct.apply(instance, arguments);
return instance;
};
相信大家看到了SomeComponent组件实例其实就是里面的instance,而instance就是通过new Constructor来返回的。也就是说,我们的ReactCompositeComponent组件实例的构造函数就是这个Constructor。虽然组件实例的构造函数是它,但是实际的实例化工作并不是它来完成的。它只是一个“空壳公司”,啥事也没干。两处代码可证:
// 函数声明
var Constructor = function() {};
// 实际的实例化
instance.construct.apply(instance, arguments);
我们可以看到,Constructor函数只做了声明,并没有具体的实现代码。它最后在闭包里面,把实例化的工作交给了实例对象的construct方法。而new出来的实例对象[自身属性]上根本没有该方法,于是乎,我们就得往原型链上去找这个方法了。
在createClass方法的源码的开头处,我们可以看到有两个地方是往构造函数的原型对象上挂载方法的。
第一个:Constructor.prototype = new ReactCompositeComponentBase();
第二个:mixSpecIntoComponent(Constructor, spec);
显然,我们传入的spec对象里面并没有construct方法,那肯定是在ReactCompositeComponentBase类里面了。一番代码导航追溯下来,我们发现了这个construct方法是ReactCompositeComponentMixin.construct:
construct: function(initialProps, children) {
// Children can be either an array or more than one argument
ReactComponent.Mixin.construct.apply(this, arguments);
this.state = null;
this._pendingState = null;
this._compositeLifeCycleState = null;
},
而方法的主体其实是由ReactComponent.Mixin.construct方法来充当的:
/**
* Base constructor for all React component.
*
* Subclasses that override this method should make sure to invoke
* `ReactComponent.Mixin.construct.call(this, ...)`.
*
* @param {?object} initialProps
* @param {*} children
* @internal
*/
construct: function(initialProps, children) {
this.props = initialProps || {};
// Record the component responsible for creating this component.
this.props.__owner__ = ReactCurrentOwner.current;
// All components start unmounted.
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
this._pendingProps = null;
this._pendingCallbacks = null;
// Children can be more than one argument
// 从这段代码可以看出,this.props.children值的类型是:对象 或者 对象组成的数组
var childrenLength = arguments.length - 1;
if (childrenLength === 1) {
if ("production" !== process.env.NODE_ENV) {
validateChildKeys(children);
}
this.props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
if ("production" !== process.env.NODE_ENV) {
validateChildKeys(arguments[i + 1]);
}
childArray[i] = arguments[i + 1];
}
this.props.children = childArray;
}
}
因为在createClass方法里面Constructor构造函数和ReactCompositeComponentBase构造函数都是个空函数,所以我们可以用伪代码做个总结一下ReactCompositeComponent组件的实例化过程就是:
ReactCompositeComponent实例化 = ReactCompositeComponentMixin.construct() + ReactComponent.Mixin.construct()
具体的实例化细节,我在这里就不深入讲述了。不过,有一点我们倒是可以再看看,那就ReactComponent.Mixin.construct方法的注释:
Base constructor for all React component.
我们可以看出,react中所有类型组件的实例化接口都是一样的,都是:
(initialProps, children) => componentInstance
ReactCompositeComponent组件的实例化过程所涉及的两个函数调用都是由用户来完成的。如果,从用户角度来看,ReactDOMComponent组件的实例化过程就不是这样了。因为react帮我们做了第一次调用,而我们只需要做第二次调用。未完待续。。。。。。