React源码解析-JSX

23,490 阅读4分钟

React是世界上最好的前端工程师们历经多年打造的UI库

他的宗旨是:learn once, write anywhere

得益于优秀的设计,react可以开发:

  • web
  • native app
  • vr
  • 桌面应用
  • 3d应用

在每个平台上,我们都可以使用JSX

一. 什么是JSX

JSX是js的一种扩展,也是一种表达式。 React认为渲染逻辑 本质上 与 UI 是耦合的。

const App = <div>
  <span>您好</span>
</div>

显然浏览器,是不认识的,这个时候,我们需要babel

二. transform JSX

preset-react

preset-react是babel在react下的解决方案

preset-react包会包含以下3个包:

  • @babel/plugin-syntax-jsx

    让babel认识jsx语法

  • @babel/plugin-transform-react-jsx

    转化jsx为函数调用

    需要注意的是jsx并不是只能被编译成createElement函数,此插件可以自由配置

  • @babel/plugin-transform-react-display-name

    处理react class displayName

旧版jsx转换

先看个🌰:

function App() {
  return <div>1</div>
}

export default App;

当我们运行时,会报错:React is not defined。(内心一万匹马在奔腾)

image.png

在react17之前,jsx转换都会转换为React.createElement调用, 所以我们必须在第一行加上:

import React from "react"

新版jsx转换

17版本之后,react升级了支持了jsx runtime,从此我们再也不需要担心 react is not defined

转化前代码:

function App() {
  return <div>1</div>
}

转化后代码:

import { jsx } from 'react/jsx-runtime';

function App() {
  return jsx("div", { children: "1" })
}

注意:不要手动引入 import { jsx } from "react/jsx-runtime"

transform result

本质上,react中的jsx会转化为createElement 或者 jsx函数调用。

我们以createElement为例,先看源码方法定义:

function createElement(type, config, children) {
  // ...
}

为了了解入参,我们先看几个🌰:

1. 单一元素

jsx:

<div data-id='test-id'>
  hello world
</div>

结果:

React.createElement(
  "div",
  {
    "data-id": "test-id"
  },
  "hello world"
);

2. 嵌套标签

jsx:

<div data-id='test-id'>
  hello world
  <span className='span'>
    hello span
  </span>
</div>

结果:

React.createElement(
  "div",
  {
    "data-id": "test-id"
  },
  "hello world",
  React.createElement(
    "span",
    {
      className: "span"
    },
    "hello span"
  )
);

不难发现,createElement方法只定义了3个参数,而本次转化结果 顶层createElement方法 有4个参数,为什么呢? 其实createElement后面可以写n个参数,内部会使用arguments对象做children集合。这个我们后面分析createElement源码再聊。

3. 组件标签

jsx:

<div data-id='test-id'>
  hello world
  <span className='span'>
    hello span
  </span>
  <TestComponent
    name='trump'  
  />
</div>

结果:

React.createElement(
  "div",
  {
    "data-id": "test-id"
  },
  "hello world",
  React.createElement(
    "span",
    {
      className: "span"
    },
    "hello span"
  ),
  React.createElement(TestComponent, {
    name: "trump"
  })
);

由此可看出,createElement第一个参数type,可以是div, span,也可以是Component。

jsx -> createElement过程的源码实现,我们后面单独开章节分析,总的概括是 AST -> Optimize -> Generate createElement,17版本之前之前和之后做了较大的改动。

这里,我们只要记住,babel利用相关插件,将jsx转化为一系列函数调用,若不指定jsx runtime,默认使用createElement函数调用,反之jsx函数调用

三. createElement

核心代码

function createElement(type, config, children) {
  // ...
  const props = {};
  let key = null;
  let ref = null;
  
  // 将config属性赋值给props
  ...
  // 处理childrens,可以传入n个参数
  const childrenLength = arguments.length - 2;
  if(childrenLength === 1) {
    // 只有1个children直接赋给props
    props.children = children
  }else (childrenLength > 1) {
     // 利用arguments对象将多个children,依次循环组装成一个childrens数组
     // 将childrens数组挂载到props对象上
     ...
  }
  
  // 处理组件的defaultProps,如果props上对应的key不存在,那么使用defaultProps值
  // 注意props可以是函数组件,可以是div,可以是class component
  if (type && type.defaultProps) {
    const 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,
  );
}

说明

总的来说,createElement函数,做一些初始化工作,定义props,挂载key, ref, children,处理defaultProps,最后返回ReactElement函数调用。

四. ReactElement

核心代码

function ReactElement(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  
  // dev环境的处理,主要解决element对象的_self, _source属性不可以写,另外冻结element和props
  // ...
  return element;
}

对象说明

reactElement会返回一个元素对象,其中:

  • $$typeof: 是一个Symbol,为了防止xss攻击而设置(因为json不支持symbol)
  • type: 元素类型,可以是div,可以是函数组件,可以是类组件
  • key: props传入的key
  • ref: props传入的ref
  • props: 属性
  • _owner: 记录负责创建这个元素的组件,类型后期实际是个Fiber

五. 总结

jsx本质上是描述元素的一种数据结构,最终将被babel转化为createElement函数或jsx函数调用,最后返回element对象。他本身不包括组件的state, 也不包括lane模型,不包括调和阶段信息

jsx在babel层面的解析,我将在后面的章节单独分享。下面的文章,我们开启react源码解析之旅吧~