React源码系列(二)深入理解jsx

562 阅读5分钟

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战  在这篇我们主要来讲解JSX,从以下三个问题入手

  1. 什么JSX?他和js有什么关系?
  2. JSX底层原理是什么?
  3. 为什么React要选择JSX

什么是JSX?

借用官网的一句话:它被称为 JSX,是一个 JavaScript 的语法扩展。JSX作为描述组件内容的数据结构,为JavaScript赋予了更多的视觉表现力。

JSX底层原理是什么?

我们在Babel官网上进行尝试,在左侧输入我们的jsx内容,右侧会对其编译成如下内容:

由图可知,JSX在编译时会被Babel编译为React.createElement方法。也就说明了我们在组件中即使没有使用React模块,为什么还要引入他,因为我们的JSX被编译后会使用它,不然会报错,而在React17.0后我们不需要引入React模块也能使用JSX(如果要使用React其他功能就需要引入),具体解释可以见官网:介绍全新的 JSX 转换

createElement

从上面得知JSX底层就是React.createElement方法,那我们来看看这个方法是怎么实现的

 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
/**
 * 
 * @param {*} type 类型:html标签类型(div)、组件(函数组件、类组件),告诉我们他是个什么类型的组件
 * @param {*} config 传入的配置:props、classname等
 * @param {*} children :子节点(文本内容,子组件)
 * @returns 
 */
export function createElement(type, config, children) {
  // propName 变量用于储存后面需要用到的元素属性
  let propName;

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

  // React 元素属性
  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // 传入了 config ==> props classname style等
  if (config != null) {
    // 依次对 ref、key、self 和 source 属性赋值
    if (hasValidRef(config)) {
      ref = config.ref;

      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      if (__DEV__) {
        checkKeyStringCoercion(config.key);
      }
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    
    // RESERVED_PROPS 是 key、ref、self、source
    // 除了上面四个属性,将剩下的其他属性依次添加到我们声明的 props 对象中,
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 获取子节点,除了type、config后的参数都将作为子节点
  const childrenLength = arguments.length - 2;
  // 只有一个一个子节点,直接赋值给 props.children 属性
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    // 不止一个子节点时,便利存到一个数组中,在后面赋值给 props.children 属性
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    // 开发环境时不允许修改 childArray 数组
    // 见[Object.freeze](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    // 将我们得到的 children 子节点(数组) 赋值给 props.children 属性
    props.children = childArray;
  }

  // 处理 defaultProps(设置的默认值),处理组件时,有自己默认的 defaultProps,将它与我们定义的 props 进行合并
  /**
      const FunctionComponent = (props) => <div className='function-title'>FunctionComponent, {props.name} </div>
   * FunctionComponent.defaultProps = { name: 'xiong' }
   */
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      // props 没传,就以 defaultProps 为准;传了就以 props 为准
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  
  // 省略...  对开发环境的处理
  
  // ReactElement 函数就是对我们的参数合并,然后添加 React节点的标识符 $$typeof: REACT_ELEMENT_TYPE;_owner 等属性
  // 返回一个 ReactElement 元素
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

ReactElement

const ReactElement = function(type, key, ref, self, source, owner, props) {
  // 将我们传入的参数组合成 element,
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE, // React节点的标识符

    // Built-in properties that belong on the element
    type: type, // 对应的html的标签
    key: key,
    ref: ref,
    props: props,

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

  // ....
  // 开发环境对我们的参数进行冻结,不许修改 Object.freeze

  // 返回我们生成的包含组件数据的对象 element
  return element;
};

看完以上两个函数,我们是否可以大胆猜测,我们在写了JSXReact对应帮我们做了哪些事,JSX被转换成了什么?

  1. JSX会被解析成React.createElement方法嵌套调用,该方法接受多个参数,第一个参数代表类型(标签名、组件名),第二个是传入的props、classname等,第三个及以后是他的子节点
  2. React.createElement方法会做什么操作呢?首先对ref、key、self 和 source 属性赋值,然后从config中取出其他属性放入到props中,接着去解析他的子节点(可能有多个),作为props.children,并且将props和组件的defaultProps合并,最后调用ReactElement函数,将我们组合好的数据传入
  3. ReactElement函数对我们的参数进行合并,重点是加入React节点的标识符

$$typeof: REACT_ELEMENT_TYPE,返回组合好的对象

const jsx = <div className='jsx-title'>jsx</div>

// ===>

{
  $$typeof: Symbol(react.element)
  key: null
  props: {className: 'jsx-title', children: 'jsx'}
  ref: null
  type: "div" // 标签名、类名、函数、匿名函数(() => {})
  _owner: null
  _store: {validated: true}
  _self: undefined
  _source: {fileName: '/Users/xiongling/Desktop/react-analysis/react-demo/src/App.js'
}

至此,我们的JSX分析完毕,最终就会被解析成一个React Element对象。

为什么React要选择JSX?

其实更准确的说是我们为什么要使用JSX?首先如果我们不适用JSX,那我们就要在代码里面写上百个React.crearteElement,各种循环嵌套,可读性也不高,一不小心就写错了,很有可能写不明白(遭到开发者的弃用)。所以当我们使用JSX语法糖后,可以让开发者使用熟悉的HTML标签来创建虚拟DOM,降低学习成本,提升开发效率与开发体验。

其他知识

如何判断React.createElement返回的对象就是React Element?

在React中提供了一个全局API React.isValidElement,一个非null对象并且有React节点的标识符$$typeof: REACT_ELEMENT_TYPE,那么他就是合法的React Element

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

如何区分类组件和函数组件?

类组件和函数组件都会变成React Element对象,其中有一个type属性标识他的类型,类组件就是他的类名,函数组件就是函数名或者匿名函数

AppClass instanceof Function === true;

AppFunc instanceof Function === true;

有上面可知无法通过引用类型区分ClassComponentFunctionComponent。那么React是如何区分的呢?React通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent

ClassComponent.prototype.isReactComponent = {};

JSX与Fiber节点是一样的吗?

不是一样的,JSX是一种描述当前组件内容的数据结构,他不包含组件schedulereconcilerender所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer标记

参考文章:

  1. React技术揭秘
  2. 【重识前端】React源码阅读(一)什么是jsx