Bootstrap

React源码之React.createElement

前言

React特点的之一就是JSX,JSX是JavaScript的语法的扩展,使用JSX来开发UI内容。React开发不一定需要使用JSX,但是使用JSX会非常便捷。

实际上JSX是React.createElement函数的语法糖,使用JSX需要使用Babel来将JSX转移成createElement函数调用(React版本号是17.0.0)。

createElement具体执行逻辑

JSX的具体使用可以查看React中文官网文档对其的介绍,这里就不赘述了。使用React JSX方式来创建的简单的实例如下:

ReactDOM.render(
  <h1>Hello World</h1>,
  document.getElementById('app')
);

在代码中直接JSX,通过Babel来配合会将JSX转移成React.createElement函数调用,上面简单实例函数调用形式如下:

ReactDOM.render(
  React.createElement('h1', null, 'Hello World'),
  document.getElementById('app')
)

接下来就具体看看React.createElement函数的具体执行逻辑。

React.createElement函数的语法如下:

React.createElement(
  type,
  [props],
  [...children]
)

React.createElement函数是用于创建并返回指定类型的新 React 元素。type表示元素名称,props表示组件的入参即传入组件的属性,children表示子组件。

createElement函数属于React核心功能,位于react core包中。createElement在源码中实际上是调用createElementWithValidation函数,具体如下:

var createElement$1 =  createElementWithValidation;
exports.createElement = createElement$1;

createElementWithValidation函数的核心逻辑具体如下:

在这里插入图片描述

isValidElementType

该函数是用于校验createElement函数传入的type参数,其具体逻辑如下:

  function isValidElementType(type) {
    if (
      typeof type === 'string' ||
      typeof type === 'function'
    ) {
      return true;
    }
    if (
      type === exports.Fragment ||
      type === exports.Profiler ||
      type === exports.unstable_DebugTracingMode ||
      type === exports.StrictMode ||
      type === exports.Suspense ||
      type === exports.unstable_SuspenseList ||
      type === exports.unstable_LegacyHidden || enableScopeAPI ) {
      return true;
    }
    // 其他校验逻辑type.$$typeof的校验
    return false;
  }

类型参数type可以是字符串、函数,也可以是React提供的一些内置组件类型。实际上类型参数type可以是:

  • 标签名字符串:浏览器原生标签
  • React 组件:class 组件或函数组件
  • React fragment、React Profiler等内置组件
createElement核心逻辑
var element = createElement.apply(this, arguments);

对于React.createElement函数调用逻辑中核心逻辑是调用createElement函数。该函数的主要逻辑可以概括如下几点:

  • 处理props选项以及其他相关参数
  • 处理children选项,支持数组和多个参数形式
  • 调用ReactElement函数

主要逻辑源代码如下:

  function createElement(type, config, children) {
    // 定义props对象
    var props = {};
    if (config != null) {
      // 将定义的props复制到内部props对象中,其中不包括key、ref、self、source属性
      for (propName in config) {
        if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
          props[propName] = config[propName];
        }
      }
    }
    // 处理
    if (type && type.defaultProps) {
    	// 相关处理逻辑
    }
    return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
  }

ReactElement函数实际上就是创建一个冻结的对象,其具体代码如下:

  var ReactElement = function (type, key, ref, self, source, owner, props) {
    var element = {
      $$typeof: REACT_ELEMENT_TYPE,
      type: type,
      key: key,
      ref: ref,
      props: props,
      _owner: owner
    };
    element._store = {}; 
    Object.defineProperty(element._store, 'validated', {
        configurable: false,
        enumerable: false,
        writable: true,
        value: false
      }); 
     Object.defineProperty(element, '_self', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: self
      });
      Object.defineProperty(element, '_source', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: source
      });

      if (Object.freeze) {
        Object.freeze(element.props);
        Object.freeze(element);
      }
    }

    return element;
  };

ReactElement函数实际上就是创建一个对象并返回,这个对象被称为React 元素,它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。

在React元素中有一个type属性,实际上就是createElement调用时的第一个参数,即该属性实际上就是保存对应的组件内容

ReactElement函数有一个重要的逻辑就是调用Object.freeze,对props和对象本身进行冻结,来避免对其再修改等操作,但是Object.freeze是浅层的冻结,如果对象内部存在引用类型是可以被修改的。

虽然props会被浅层冻结,但是不是完全冻结,还是可以更改内部对象的,但是React声明props的只读性即:

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改

validatePropTypes

处理组件的propTypes做相关处理,这里逻辑次要可以略过。

总结

React.createElement函数的逻辑相对来说是简单清晰的,总结如下:

  • 校验类型参数type的合法性,可以是代表标签的字符串、组件(class组件、函数组件)、一些内部组件
  • 生成表示组件类型的对应React元素对象,并处理相关属性
    • 子组件children选项的传参的数组、非数组方式处理
    • props相关处理(key、ref、self、source不会被包含props,如果包含defaultProps也会立即处理),其中children会是props中特殊的一个属性

这里简单对比下Vue,实际上Vue中也存在一个createElement函数是用于创建虚拟DOM对象的,其createElement函数的传入参数与React这边基本相同。其中对于相同位置的props参数选项,Vue称之为数据对象,是创建组件非常重要的对象参数。不同于Vue数据对象的复杂性,React.createElement函数调用传入的props参数是非常清晰简单的:

所有关于组件的属性和事件都保存在React元素对象中的props属性中,而没有其他复杂的区分例如props、attrs、domProps、nativeOn等

当然Vue数据对象的复杂性也是由于Vue事件机制等决定的,其在解析阶段就区分了原生属性、自定义属性、原生事件、自定义事件。React这边对于这里的区分逻辑,理论上也是存在的,目前看来是在后续render处理阶段,这里先暂时不去关心之后会去细看。

无论是React.createElement还是Vue实例的createElement都是生成对应的虚拟DOM,但是这个过程Vue的处理是非常复杂的。

;