深入react组件初始挂载

本文研究的版本为reactv0.8.0

    在v0.8.0版本中,react组件初始挂载实现是相对简单的。总体可以划分为两部分来阐述:组件实例化和(真正的)组件挂载。因为先理解组件实例化,再理解组件挂载会比较好。所以,我先介绍组件实例化,后介绍(真正的)组件挂载流程。


组件实例化

什么是react组件?

在搞懂什么是react组件之前,我们不妨先了解一下“组件”的定义。显然,“组件”这个概念并不是软件编程界所独有的,它应该是来源于工程学。

卡耐基梅隆大学给“组件”下过这样的定义:一个不透明的功能实体,能够被第三方组装,且符合一个构件模型。

计算机百科全书是这样说的:是软件系统中具有相对独立功能、接口由契约指定、和语境有明显依赖关系、可独立部署、可组装的软件实体。

软件构件著作中如是定义:是一个组装单元,它具有约定式规范的接口,以及明确的依赖环境。构建可以被独立的部署,由第三方组装。

不同的上下文中,“组件”的定义是略有不同的。但是,共同点是有的。“功能独立,向外提供接口,可组装/组合”就是组件定义的基本要素。我们拿这三点对照一下后,会发现react组件是符合这三个基本要素的。

1.react组件是可组合的。例如我们会有这样的应用代码:

const A_Component = React.createClass({

    render(){

        return (

            <B_Component>

                <C_Component />

            </B_Component>

        )

    }

})

这种示例下,我们可以清晰地看到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');

这几个类型组件的实例都通过__proto__属性告知我们,这些实例对象都是通过原型继承来继承了某些方法的。因为没有时间用UML画更具体的类关系图,我画了个简单粗略的关系图:

在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 <SomeComponent />;

    }

})

我们大家都知道jsx<SomeComponent />会被编译为一个普通的函数调用模样:SomeComponent()。也就是看似声明式的jsx本质是一个命令式的函数调用,就像react的提供给用户的是函数式的开发风格下其实是面向对象式的实现一样的道理:真相往往是dirty的。而这个函数调用就是我们上面所提两次调用里面的最后一次了。SomeComponent()返回的是什么呢?是组件实例。什么?都没有new操作符,如何实例化的呢?待我娓娓道来。

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帮我们做了第一次调用,而我们只需要做第二次调用。未完待续。。。。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容