JSX的三“大问题”

JSX作为React框架的一大特色,它与React本身的运行机制存在着千丝万缕的联系。在揭开这层“联系”的面纱之前,我们先尝试思考关于JSX的三个“大问题”:

  • JSXJS之间的关系是什么?它的本质是什么?
  • React选用JSX的动机是什么?它解决了什么问题?
  • JSX背后的功能模块是什么?它做了什么?

下面,我们通过问题去摸索JSX背后的一个个小故事,了解它的源头。

一、JSX的本质

React官网给了定义:

JSX is a syntax extension to JavaScript. It is similar to a template language, but it has full power of JavaScript.

but前半句我们不难理解:JSXJS的一种语法扩展,它和模版语言很接近。既然是but,那重点当然在后半句:JSX充分具备JS的能力。

嗯哼?JSX怎么具备了JS的能力?它是如何在JS中生效的?不急,再看看官方说法:

JSX gets compiled to React.createElement() calls which return plain JavaScript objects called “React elements”.

JSX会被编译为React.createElement(),React.createElement()将返回一个叫做React elementsJS对象。

编译这个动作主要由工具链Babel完成,它具备将JSX语法转换为JS代码的能力,也就意味着:我们写着“看起来像HTMLJSX其实写的是React.createElement()

JSX的本质是React.createElement()调用的语法糖。呼应了官方回应的JSX充分具备JS的能力。

不信?打开Babel Try it out看看Babel编译后的JSX:

ff096da0a9184c90undefined

二、React选用JSX的动机

我们换个角度,如果React不选用JSX,直接使用React.createElement()会是怎样的效果:
0af3590d30ffdd8dundefined

首先,视觉上,左边的代码相对右边的,代码层次分明,嵌套清晰,写起来也清爽一些

其次,JSX语法糖允许开发者使用熟悉的类HTML标签来创建虚拟DOM

选用JSX,不仅降低学习成本,同时也提升了研发效率和体验。

三、JSX背后的React.createElement()

从第一个问题我们剖析了JSX的本质是React.createElement()调用的语法糖,在深入源码探索之前,先看看React.createElement()在逻辑层面的任务流转:

  • 第一步:二次处理key、ref、self、source四个属性值
  • 第二步:遍历config,筛选出可推进props里的属性
  • 第三步:提取子元素,推入childArray(即:props.children)数组
  • 第四步:格式化defaltProps
  • 第五步:结合以上数据作为入参,发起ReactElement调用

可以看出,React.createElement()中并未涉及到算法或者复杂的DOM操作,它的每一个步骤都在格式化数据,就像是开发者和ReactElement调用之间的一个“转换器”、一个数据处理层。最后通过调用ReactElement实现元素的创建。

ReactElement它只做一件事,就是“创建”,将传入的参数按照一定的规范,“组装”到element对象里,返回给React.createElement()

ReactElement返回的element对象,实质上是以JS对象形式存在的DOM的描述,即我们常说的“虚拟DOM”,准确的说是虚拟DOM中的一个节点。

从下图虚拟DOMReact中的形态可验证:
33c68b3e328ef7a3undefined

就这个示例,我们至少达成两个共识:

  • 虚拟DOMJS对象
  • 虚拟DOM是对真实DOM的描述

话说到这了,不看代码是不是无法消除你心里的梗?你可以直接链接到createElement的源码ReactElement的源码,也可以同我一起看看这个“数据中介”createElement的源码实现:

/**
 * 参数解析
 * @param {*} type 用于标识节点的类型,它可以是html标签字符串,也可以是react组件类型或Reactfragment类型
 * @param {object} config 组件所有的属性都会以键值对的形式存储在config对象中
 * @param {object} children 记录组件标签之间嵌套的内容,也就是所谓的子元素、子节点
 */
export function createElement(type, config, children) {
  // propName 变量用于储存后面需要用到的元素属性
  let propName;

  // props 变量用于储存元素属性的键值对集合
  const props = {};

  // key、ref、self、source 均为 React 元素的属性,此处不必深究
  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // config 对象中存储的是元素的属性
  if (config != null) {
    // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值      
    if (hasValidRef(config)) {
      ref = config.ref;

      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    // 此处将 key 值字符串化
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 接着把 config 里面的属性一个一个挪到 props 这个之前声明好的对象里面
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // childrenLength 指的是当前元素的子元素个数,减去的 2 是 type 和 config 两个参数占用的长度
  const childrenLength = arguments.length - 2;
  // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  if (childrenLength === 1) {
    // 直接把这个参数的值赋给props.children
    props.children = children;
    // 处理嵌套多个子元素的情况
  } else if (childrenLength > 1) {
    // 声明一个子元素数组
    const childArray = Array(childrenLength);
    // 把子元素推进数组里
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    // 最后把这个数组赋值给props.children
    props.children = childArray;
  }

  // 处理 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }

  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}


const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录创造该元素的组件
    _owner: owner,
  };

  // 此处针对__DEV__环境下进行的一些处理,对于理解逻辑意义不大,可暂且省略
  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};