组件和对象的创建

1 组件的创建

React受大家欢迎的一个重要原因就是可以自定义组件。这样的一方面可以复用开发好的组件,实现一处开发,处处调用,另外也能使用别人开发好的组件,提高封装性。另一方面使得代码结构很清晰,组件间耦合减少,方便维护。ES5创建组件时,调用React.createClass()即可. ES6中使用class myComponent extends React.Component, 其实内部还是调用createClass创建组件。

组件创建我们可以简单类比为Java中ClassLoader加载class。下面来分析下createClass的源码,我们省去了开发阶段错误提示的相关代码,如propType的检查。(if (“development” !== ‘production’) {}代码段都不进行分析了,这些只在开发调试阶段调用)

createClass: function (spec) {
    var Constructor = function (props, context, updater) {
      // 触发自动绑定
      if (this.__reactAutoBindPairs.length) {
        bindAutoBindMethods(this);
      }

      // 初始化参数
      this.props = props;
      this.context = context;
      this.refs = emptyObject;  // 本组件对象的引用,可以利用它来调用组件的方法
      this.updater = updater || ReactNoopUpdateQueue;

      // 调用getInitialState()来初始化state变量
      this.state = null;
      var initialState = this.getInitialState ? this.getInitialState() : null;
      this.state = initialState;
    };

    // 继承父类
    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];

    injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));

    mixSpecIntoComponent(Constructor, spec);

    // 调用getDefaultProps,并挂载到组件类上。defaultProps是类变量,使用ES6写法时更清晰
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }

    // React中暴露给应用调用的方法,如render componentWillMount。
    // 如果应用未设置,则将他们设为null
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }

    return Constructor;
  },

createClass主要做的事情有

定义构造方法Constructor,构造方法中进行props,refs等的初始化,并调用getInitialState来初始化state
调用getDefaultProps,并放在defaultProps类变量上。这个变量不属于某个单独的对象。可理解为static 变量
将React中暴露给应用,但应用中没有设置的方法,设置为null。

2 对象的创建

JSX中创建React元素最终会被babel转译为createElement(type, config, children), babel根据JSX中标签的首字母来判断是原生DOM组件,还是自定义React组件。如果首字母大写,则为React组件。这也是为什么ES6中React组件类名必须大写的原因。如下面代码

<div className="title" ref="example">
    <span>123</span>    // 原生DOM组件,首字母小写
    <ErrorPage title='123456' desc={[]}/>    // 自定义组件,首字母大写
</div>

转译完后是

React.createElement(
        'div',    // type,标签名,原生DOM对象为String
        {
            className: 'title',
            ref: 'example'
        },   // config,属性
        React.createElement('span', null, '123'),   // children,子元素
        React.createElement(
            // type,标签名,React自定义组件的type不为String.
            // _errorPage2.default为从其他文件中引入的React组件
            _errorPage2.default,    
            {
                title: '123456',
                desc: []
            }
        )   // children,子元素
)

下面来分析下createElement的源码

ReactElement.createElement = function (type, config, children) {
  var propName;

  // 初始化参数
  var props = {};

  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  // 从config中提取出内容,如ref key props
  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;

    // 提取出config中的prop,放入props变量中
    for (propName in config) {
      if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

  // 处理children,挂到props的children属性下
  // 入参的前两个为type和config,后面的就都是children参数了。故需要减2
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    // 只有一个参数时,直接挂到children属性下,不是array的方式
    props.children = children;
  } else if (childrenLength > 1) {
    // 不止一个时,放到array中,然后将array挂到children属性下
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 取出组件类中的静态变量defaultProps,并给未在JSX中设置值的属性设置默认值
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 返回一个ReactElement对象
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};

下面来看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,

    // ReactElement对象上的四个变量,特别关键
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner
  };
    return element;
}

可以看到仅仅是给ReactElement对象内的成员变量赋值而已,不在赘述。

3 组件对象初始化

在mountComponent()挂载组件中,会进行组件渲染,调用到instantiateReactComponent()方法。这个过程我们在React生命周期方法中再详细讲述,这里有个大体了解即可。instantiateReactComponent()根据ReactElement中不同的type字段,创建不同类型的组件对象。源码如下

// 初始化组件对象,node是一个ReactElement对象,是节点元素在React中的表示
function instantiateReactComponent(node) {
  var instance;

  var isEmpty = node === null || node === false;
  if (isEmpty) {
    // 空对象
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    // 组件对象,包括DOM原生的和React自定义组件
    var element = node;

    // 根据ReactElement中的type字段区分
    if (typeof element.type === 'string') {
      // type为string则表示DOM原生对象,比如div span等。可以参看上面babel转译的那段代码
      instance = ReactNativeComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // 保留给以后版本使用,此处暂时不会涉及到
      instance = new element.type(element);
    } else {
      // React自定义组件
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 元素是一个string时,对应的比如<span>123</span> 中的123
    // 本质上它不是一个ReactElement,但为了统一,也按照同样流程处理,称为ReactDOMTextComponent
    instance = ReactNativeComponent.createInstanceForText(node);
  } else {
    // 无处理
  }

  // 初始化参数,这两个参数是DOM diff时用到的
  instance._mountIndex = 0;
  instance._mountImage = null;

  return instance;
}

故我们可以看到有四种创建组件元素的方式,同时对应四种ReactElement

ReactEmptyComponent.create(), 创建空对象ReactDOMEmptyComponent
ReactNativeComponent.createInternalComponent(), 创建DOM原生对象 ReactDOMComponent
new ReactCompositeComponentWrapper(), 创建React自定义对象ReactCompositeComponent
ReactNativeComponent.createInstanceForText(), 创建文本对象 ReactDOMTextComponent
下面分别分析这几种对象,和创建他们的过程。

ReactDOMEmptyComponent

由ReactEmptyComponent.create()创建,最终生成ReactDOMEmptyComponent对象,源码如下

var emptyComponentFactory;

var ReactEmptyComponentInjection = {
  injectEmptyComponentFactory: function (factory) {
    emptyComponentFactory = factory;
  }
};

var ReactEmptyComponent = {
  create: function (instantiate) {
    return emptyComponentFactory(instantiate);
  }
};

ReactEmptyComponent.injection = ReactEmptyComponentInjection;

ReactInjection.EmptyComponent.injectEmptyComponentFactory(function (instantiate) {
  // 前面比较绕,关键就是这句话,创建ReactDOMEmptyComponent对象
   return new ReactDOMEmptyComponent(instantiate);
});

// 各种null,就不分析了
var ReactDOMEmptyComponent = function (instantiate) {
  this._currentElement = null;
  this._nativeNode = null;
  this._nativeParent = null;
  this._nativeContainerInfo = null;
  this._domID = null;
};

ReactDOMComponent

由ReactNativeComponent.createInternalComponent()创建。这里注意原生组件不代表是DOM组件,而是React封装过的Virtual DOM对象。React并不直接操作原生DOM。

大家可以自己看ReactDOMComponent的源码。重点看下ReactDOMComponent.Mixin

ReactDOMComponent.Mixin = {
  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {},
  _createOpenTagMarkupAndPutListeners: function (transaction, props){},
  _createContentMarkup: function (transaction, props, context) {},
  _createInitialChildren: function (transaction, props, context, lazyTree) {}
  receiveComponent: function (nextElement, transaction, context) {},
  updateComponent: function (transaction, prevElement, nextElement, context) {},
  _updateDOMProperties: function (lastProps, nextProps, transaction) {},
  _updateDOMChildren: function (lastProps, nextProps, transaction, context) {},
  getNativeNode: function () {},
  unmountComponent: function (safely) {},
  getPublicInstance: function () {}
}

其中暴露给外部的比较关键的是mountComponent,receiveComponen, updateComponent,unmountComponent。他们会引发React生命周期方法的调用,下一节再讲。

ReactCompositeComponent

由new ReactCompositeComponentWrapper()创建,重点看下ReactCompositeComponentMixin

var ReactCompositeComponentMixin = {
  // new对应的方法,创建ReactCompositeComponent对象
  construct: function(element) {},
  mountComponent,   // 初始挂载组件时被调用,仅一次
  performInitialMountWithErrorHandling, // 和performInitialMount相近,只是多了错误处理
  performInitialMount,  // 执行mountComponent的渲染阶段,会调用到instantiateReactComponent,从而进入初始化React组件的入口
  getNativeNode,
  unmountComponent, // 卸载组件,内存释放等工作
  receiveComponent,
  performUpdateIfNecessary,
  updateComponent,  // setState后被调用,重新渲染组件
  attachRef,    // 将ref指向组件对象,这样我们就可以利用它调用对象内的方法了
  detachRef,    // 将组件的引用从全局对象refs中删掉,这样我们就不能利用ref找到组件对象了
  instantiateReactComponent,    // 初始化React组件的入口,在mountComponent时的渲染阶段会被调用
}

ReactDOMTextComponent

由ReactNativeComponent.createInstanceForText()创建,我们也不细细分析了,主要入口代码如下,大家可以自行分析。

var ReactDOMTextComponent = function (text) {
  this._currentElement = text;
  this._stringText = '' + text;
};

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