React源码(一):认识JSX

什么是JSX

在开发React项目时,我们都会在JSX文件中编写组件。但是JSX文件在js环境中运行会发生报错,这是因为在React项目中,在编译阶段Babel就已经将JSX结构处理成ReactElement格式。

通过Babel的官方网站,我们可以体验实时的编译功能,可以直接输入 JSX 代码查看转换结果。 可以看到,JSX结构已经被编译成了React.createElement形式。

React.createElement

createElement在react/index下被导出:

js 复制代码
// package/react/index.js
export { createElement } from '.src/ReactClient';

在react/src/ReactClient下被引入:

js 复制代码
// package/react/src/ReactClient
import {
  createElement,
} from './jsx/ReactJSXElement';

最后我们就能找到createElement定义的地方,在ReactJSXElement这个文件中

js 复制代码
// package/react/src/jsx/ReactJSXElement
export function createElement(type, config, children) {
    // ......
}

入参解读

现在让我们一起来看一下React.createElement这个api的用法

React.createElement( type, config, children )

  • 第一个参数type:如果是组件类型,会传入组件对应的类或函数;如果是 DOM 元素类型,则传 入 div 或者 span 之类的字符串。
  • 第二个参数config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
  • 其他参数children:依次为该元素的子元素,根据顺序排列。返回结果为 React Element 对象。

createElement是怎么工作的

createElemt的工作可以大致分为以下四步:

  1. 初始化参数,type、config
  2. 处理config中的其他属性
  3. 处理children,形成扁平化的children数组
  4. 创建React Element对象

看一下代码的大致实现:

js 复制代码
/**
 * 创建 React 元素的核心函数
 * @param {string|Function} type - 元素类型,可以是原生标签字符串(如'div')或 React 组件
 * @param {Object|null} config - 包含元素属性的配置对象
 * @param {...any} children - 子元素,可以是任意数量的参数
 * @returns {Object} - 返回一个 React 元素对象
 */
export function createElement(type, config, children) {
  /* 1. 初始化参数,type、config */
  let propName; // 用于遍历属性名的临时变量

  // 创建一个空对象来存储处理后的 props
  const props = {};

  let key = null; // 初始化 key 为 null

  // 如果传入了 config 对象(即属性对象不为空)
  if (config != null) {
    // 检查并处理有效的 key
    if (hasValidKey(config)) {
      key = '' + config.key; // 将 key 强制转换为字符串
    }

    /* 2. 处理config中的其他属性 */
    // 将剩余的属性添加到新的 props 对象中
    for (propName in config) {
      // 检查是否是 config 自身的属性(非原型链上的)
      if (
        hasOwnProperty.call(config, propName) &&
        // 跳过保留的属性名
        propName !== 'key' && // key 已经单独处理
        // 尽管我们在运行时不再使用这些属性,但我们不希望它们出现在 props 中,
        // 所以在 createElement 中过滤掉它们。
        // 在 jsx() 运行时不需要这样做,因为 jsx() 转换从不将这些作为 props 传递;
        // 它使用单独的参数。
        propName !== '__self' &&
        propName !== '__source'
      ) {
        props[propName] = config[propName]; // 将属性复制到 props 对象
      }
    }
  }
  
  /* 3. 处理children,形成扁平化的children数组 */
  // 处理子元素:可以有多个参数,这些参数会被转移到新分配的 props 对象上
  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]; // 收集所有子元素
    }
    props.children = childArray; // 将子元素数组赋值给 props.children
  }

  // 解析默认 props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      // 只有当 props 中该属性为 undefined 时才应用默认值
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  
  /* 4. 创建React Element对象 */
  // 创建并返回 React 元素对象
  return ReactElement(
    type, // 元素类型
    key, // 元素的 key
    undefined, // ref(在这里未处理,会在 ReactElement 函数中处理)
    undefined, // 未使用
    getOwner(), // 获取当前所有者(用于上下文和调试)
    props, // 处理后的 props 对象
  );
}

总结

由此可见,JSX其实是React.createElement语法糖,而createElement作为一个函数,其实就是根据入参,返回一个Reacte Element对象。

React Element

JSX在编译阶段会转换成createElemnt的形式,执行createElement则会生成React ELement对象,该对象包含包含以下属性:

js 复制代码
   const element = { 
     $$typeof: REACT_ELEMENT_TYPE, // Element 元素类型,Symbol值
     type: type, // type 属性,证明是DOM元素还是自定义组件 
     key: key, // key 属性 
     ref: ref, // ref 属性,获取对实际 DOM 节点或组件实例的引用
     props: props, // props 属性,包含元素的属性和子元素
     ... 
   };

在React项目中打印一个JSX元素

js 复制代码
  const AppJsx = (
    <div id='root' className='root'>
      <span className='span'>123</span>
      <p id='p'>this is p</p>
      'today is Sunday'
    </div>
  )
  
  console.log(AppJsx);

在控制台中可以得到:

Babel工作流程

JSX语法的编译实现是由Babel插件实现的:

  1. @babel/plugin-syntax-jsx :仅允许 Babel 解析 JSX 语法(不转换),为后续插件提供 AST 支持
  2. @babel/plugin-transform-react-jsx :将 JSX 转换为 React.createElement()jsx() 调用

两种Runtime的插件实现

  1. 经典运行时 Classic Runtime
  • 要求:必须在文件中显式导入 React(否则会报错)
  • 转换结果:JSX → React.createElement()
js 复制代码
function App() {
  return /*#__PURE__*/React.createElement("div", {
    id: "root",
    class: "root"
  }, /*#__PURE__*/React.createElement("span", null, "123"), /*#__PURE__*/React.createElement(Child, null), "'today is Sunday'");
}
  1. 自动运行时 Automatic Runtime
  • 优势: 无需手动导入 React
  • 转换结果:JSX → jsx()jsxs()(从 react/jsx-runtime 自动导入)
js 复制代码
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function App() {
  return /*#__PURE__*/_jsxs("div", {
    id: "root",
    class: "root",
    children: [/*#__PURE__*/_jsx("span", {
      children: "123"
    }), /*#__PURE__*/_jsx(Child, {}), "'today is Sunday'"]
  });
}
  • 配置:
js 复制代码
{
  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic",
      }
    ]
  ]
}
相关推荐
寅时码1 小时前
我开源了一款 Canvas “瑞士军刀”,十几种“特效与工具”开箱即用
前端·开源·canvas
CF14年老兵1 小时前
🚀 React 面试 20 题精选:基础 + 实战 + 代码解析
前端·react.js·redux
CF14年老兵1 小时前
2025 年每个开发人员都应该知道的 6 个 VS Code AI 工具
前端·后端·trae
十五_在努力1 小时前
参透 JavaScript —— 彻底理解 new 操作符及手写实现
前端·javascript
拾光拾趣录1 小时前
🔥99%人答不全的安全链!第5问必翻车?💥
前端·面试
IH_LZH1 小时前
kotlin小记(1)
android·java·前端·kotlin
lwlcode1 小时前
前端大数据渲染性能优化 - 分时函数的封装
前端·javascript
Java技术小馆1 小时前
MCP是怎么和大模型交互
前端·面试·架构
玲小珑1 小时前
Next.js 教程系列(二十二)代码分割与打包优化
前端·next.js
coding随想2 小时前
HTML5插入标记的秘密:如何高效操控DOM而不踩坑?
前端·html