React源码分析 - 组件初次渲染

1,625 阅读5分钟

React也写了有一段时间了,不了解下ta的原理都不好意思和别人说自己会React...所以看了一些源码分析的文章,自己也撸了一遍React的源码【真是有点绕】,算是搞明白了React的原理。

但是最近给一个妹纸解释React原理的时候,把她说蒙圈了...很受伤,本着面向妹纸编程的原则,决定还是要写成文章,再给她看看。

【本文基于 React v15.0.0】

【源码分析部分只保留production环境下的核心功能的代码】

基本概念

在react的源码中有三个概念需要先分清楚:

  • ReactElement
  • ReactComponent
  • ReactClass

ReactElement

ReactElement是描述react中的虚拟的DOM节点的对象,ReactElement主要包含了这个DOM节点的类型(type)、属性(props)和子节点(children)。ReactElement只是包含了DOM节点的数据,还没有注入对应的一些方法来完成React框架的功能。

ReactElement通过React.createElement来创建,使用jsx语法的表达式,也会被babel(react的妈妈Facebook在react刚出生的时候是有提供自己的编译器的,但是Babel之后成为了社区主要的jsx语法编译的工具)编译成对应的调用React.createElement的形式。

Babel官网上实验一下比较清楚:

const React = require('react');
const ReactDOM = require('react-dom');

const View = (
  <div
    className='wrapper--outer'
    >
    <div
      className='wrapper1--inner'
      style={{ color: '#38f' }}
      >
      hello world
    </div>
    <div
      className='wrapper2--inner'
      >
      hello world
    </div>
    <Hello />
    <Inner text="heiheihei">
      <div>yoyoyo</div>
    </Inner>
  </div>
);

ReactDOM.render(<View />, document.getElementById('app'));

Babel编译后:

'use strict';

var React = require('react');
var ReactDOM = require('react-dom');

var View = React.createElement(
  'div',
  {
    className: 'wrapper--outer'
  },
  React.createElement(
    'div',
    {
      className: 'wrapper1--inner',
      style: { color: '#38f' }
    },
    'hello world'
  ),
  React.createElement(
    'div',
    {
      className: 'wrapper2--inner'
    },
    'hello world'
  ),
  React.createElement(Hello, null),
  React.createElement(
    Inner,
    { text: 'heiheihei' },
    React.createElement(
      'div',
      null,
      'yoyoyo'
    )
  )
);

ReactDOM.render(React.createElement(View, null), document.getElementById('app'));

可以看到在jsx文件中的html写法的表达式都会被编译成调用React.createElement的形式。我们称呼jsx里面的为React的DOM标签的话,如果DOM标签的首字母为大写的时候,这个标签(类 => 自定义组件类, 函数 => 无状态组件)则会被作为参数传递给createElement;如果DOM标签的首字母为小写,则将标签名(div, span, a 等html的 DOM标签)以字符串的形式传给createElement;如果是字符串或者空的话,则直接将字符串或者null当做参数传递给createElement。

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;

  // 将参数赋给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;
    // Remaining properties are added to a new props object
    for (propName in config) {
      // 跳过React保留的参数
      if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }


  // 将子元素按照顺序赋给children的数组
  // 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;
  }

  // 对于默认的参数,判断是否有传入值,有的话直接将参数和对应的值赋给props,否则将参数和参数默认值赋给props
  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {

    // Symbol类型的tag唯一标示这个对象是一个React 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;
};

createElement基本没做什么特别的处理,返回了一个React Element对象,所以上述的栗子View最终是一个多层级的大对象(简化如下):

const View = (
  <div
    className='wrapper--outer'
    >
    <div
      className='wrapper1--inner'
      style={{ color: '#38f' }}
      >
      hello world
    </div>
    <div
      className='wrapper2--inner'
      >
      hello world
      {null}
    </div>
    <Hello />
    <Inner text="heiheihei">
      <div>yoyoyo</div>
    </Inner>
  </div>
);
---------------
{
  type: 'div',
  props: {
    className: 'wrapper--outer'
  },
  children: [
    {
      type: 'div',
      props: {
        className: 'wrapper1--inner',
        style: {
          color: '#38f'
        }
      },
      children: 'hello world'
    }, {
      type: 'div',
      props: {
        className: 'wrapper2-inner'
      },
      children: [
        'hello world',
        null
      ]
    },
    {
      type: Hello,
      props: null
    },
    {
      type: Inner,
      props: {
        text: 'heiheihei'
      },
      children: [
        {
          type: 'div',
          props: null,
          children: 'yoyoyo'
        }
      ]
    }
  ]
}

这样一个对象只是保存了DOM需要的数据,并没有对应的方法来实现React提供给我们的那些功能和特性。ReactElement主要分为DOM Elements和Component Elements两种,我们称这样的对象为ReactElement。

ReactComponent

ReactComponent是基于ReactElement创建的一个对象,这个对象保存了ReactElement的数据的同时注入了一些方法,这些方法可以用来实现我们熟知的那些React的特性。

ReactClass

ReactClass就是我们在写React的时候extends至React.Component类的自定义组件的类,如上述中的View和Inner,ReactClass实例化后调用render方法可返回ReactElement。

组件初次渲染

对于撸源码个人的习惯是代码少的话,直接看就是了;代码量大、复杂的话可以先看些文章,大致了解重点,(过一遍源码,这个看个人兴趣,最有效的还是打断点执行查看实际运行的流程)然后挑几个代表性的case打断点单步执行把执行过程再过一遍就好了。

React的源码比较绕,用了很多的依赖注入的方式去定义方法,基本上当你不清楚一个方法什么时候定义的时候,ta可能就是在

(ReactMount.js)
ReactDefaultInjection.inject();

中注入的。

这里需要注意的是在React中主要有四类组件:

  • Empty Component(空元素)
  • Text Component(文本or数字)
  • DOM Component(DOM元素)
  • Composite Component(自定义组件元素)

根据输入的ReactElement的type的类型的不同instantiateReactComponent方法会返回不同类型的Component。

在介绍了上面的一些需要了解的基本概念后,用流程图来表示React组件初次渲染如下。

  • 蓝色是输入输出
  • 橙色是一些重要的中间变量
  • 黑色调用到的函数

react

最终通过_mountImageIntoNode一次性将之前递归生成的markup渲染成真实的DOM。

该图省略了事务、事件机制和生命周期等方面的内容,这些会在之后的文章单独介绍。

参考资料