Day 2:JSX 转换原理

13 阅读3分钟

📅 Day 2:JSX 转换原理

学习目标:彻底搞懂 <div> 是怎么变成 createElement


👴 老大爷能听懂版

JSX 是个啥?

想象你要寄一封信:

实际操作JSX 相当于
你写纸质信(手写信)你写 JSX(<div>Hello</div>
邮递员把信转换成快递单Babel/编译器把 JSX 转换成 createElement
快递单才是真正寄出去的东西createElement 才是 React 真正用的

简单说:JSX 就是一种"语法糖",让你写代码更方便,底层会自动转换成 JavaScript 函数调用。


JSX 是怎么变成 createElement 的?

举个例子:

// 你写的代码(JSX)
<div className="box" onClick={handleClick}>
  <span>Hello</span>
</div>

编译器帮你转换成:

// 转换后实际运行的代码
React.createElement(
  'div',                           // 类型:div
  { className: 'box', onClick: handleClick },  // 属性
  React.createElement('span', null, 'Hello')   // 子元素
)

就像这样:

你写的 JSX:  <div>Hello</div>
                 ↓ 翻译
createElement:  createElement('div', null, 'Hello')

为什么要转来转去?

  1. 浏览器不认识 JSX — 浏览器只懂原生 JavaScript,需要转换
  2. 统一入口 — 无论是手写还是自动生成,最后都调用同一个函数
  3. 描述 UI 更爽 — 写 <div> 比写 createElement('div') 爽多了

createElement 返回啥?

返回一个对象,叫 React Element(React 元素)

// 这就是 createElement 返回的东西
{
  $$typeof: Symbol(react.element),  // 标记:我是 React 元素
  type: 'div',                       // 标签类型
  key: null,                         // 唯一标识(优化用)
  ref: null,                         // 引用(操作 DOM 用)
  props: {                           // 属性 + 子元素
    className: 'box',
    onClick: handleClick,
    children: { type: 'span', props: { children: 'Hello' } }
  }
}

把这个想象成一张"建筑蓝图",React 根据这张蓝图来盖房子(渲染 DOM)。


JSX 的几种写法

写法说明转换结果
<div>Hello</div>文本子元素createElement('div', null, 'Hello')
<div>{count}</div>变量createElement('div', null, count)
<div><span /></div>嵌套createElement('div', null, createElement('span', null))
<div {...props} />展开合并 props

Fragment 是啥?

相当于"隐形文件夹":

// 你写的
<>
  <div>A</div>
  <div>B</div>
</>

// 转换后(注意没有外层 div)
React.createElement(React.Fragment, null,
  React.createElement('div', null, 'A'),
  React.createElement('div', null, 'B')
)

作用:把多个元素包起来,但不增加额外的 DOM 节点。


💻 专业开发者版

JSX 转换流程

源代码 (.jsx)
    ↓
Babel/TSC transform@babel/plugin-transform-react-jsx)
    ↓
React.createElement() / jsx()
    ↓
ReactElement 对象
    ↓
React DOM 渲染到页面

React 19 的 JSX 运行时

React 19 引入了新的 JSX 运行时,不再依赖 React 对象:

// React 18 及之前
// 需要 import React from 'react'
<div>Hello</div>  // 转换成 React.createElement('div', null, 'Hello')

// React 19
// 不需要 import React
// 自动从 jsx-runtime 导入 jsx/jsxs
<div>Hello</div>  // 转换成 jsx('div', { children: 'Hello' })

核心源码文件

文件作用
packages/react/src/jsx/ReactJSXElement.js核心,createElement/jsx/jsxs 实现
packages/react/src/jsx/ReactJSX.jsjsx-runtime 导出
packages/react/jsx-runtime.js包级别的 jsx-runtime 入口

createElement 源码解析

// packages/react/src/jsx/ReactJSXElement.js

export function createElement(type, config, children) {
  // 1. 提取 key 和 ref(特殊属性,不进入 props)
  let key = null;
  if (config != null && hasValidKey(config)) {
    key = '' + config.key;
  }

  // 2. 构建 props 对象
  const props = {};
  for (const propName in config) {
    if (propName !== 'key' && propName !== '__self' && propName !== '__source') {
      props[propName] = config[propName];
    }
  }

  // 3. 处理 children(多个子元素变成数组)
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    props.children = Array.from({ length: childrenLength }, (_, i) => arguments[i + 2]);
  }

  // 4. 处理 defaultProps
  if (type && type.defaultProps) {
    for (const propName in type.defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = type.defaultProps[propName];
      }
    }
  }

  // 5. 调用 ReactElement 创建最终对象
  return ReactElement(type, key, props, ...);
}

ReactElement 源码

function ReactElement(type, key, props, owner, debugStack, debugTask) {
  return {
    // 唯一标识:判断是不是 React 元素
    $$typeof: REACT_ELEMENT_TYPE,
    
    // 元素类型:'div'、'span'、组件函数、组件对象
    type,
    
    // 唯一键:用于 Diff 算法优化
    key,
    
    // 引用:操作真实 DOM
    ref,
    
    // 属性 + 子元素
    props,
    
    // DEV 模式下的调试信息
    _owner: owner,
    _debugInfo: ...,
  };
}

jsx vs jsxs vs createElement

函数使用场景children 处理
jsx动态 children单个或多个,会创建数组
jsxs静态 children(编译器优化)已知是静态数组,不需要再处理
createElement手动调用总是处理
// jsx - 动态 children
jsx('div', { children: count })  // 运行时才知道 children

// jsxs - 静态 children  
jsxs('div', {}, child1, child2, child3)  // 编译时就知道是静态数组

// createElement - 手动调用
createElement('div', { className: 'box' }, 'hello')

📋 面试必考点

Q1:JSX 和 createElement 的关系?

答: JSX 是一种语法糖,底层通过 Babel 编译转换为 createElement 函数调用。主要目的是让 UI 代码更易读、写起来更爽。

Q2:React Element 和 DOM Element 的区别?

答:

  • React Element 是"蓝图"(描述 UI 的 JS 对象)
  • DOM Element 是"房子"(真实渲染到页面的节点)
  • React 根据 React Element 来创建/更新 DOM Element

Q3:为什么不能用 index 作为 key?

答:

  • 如果列表项的顺序会变化,用 index 作为 key 会导致 React 错误地复用 DOM 节点
  • 可能引起 UI 错乱、状态错误、动画异常等问题
  • 正确做法:用唯一 ID 或稳定的数据标识作为 key

Q4:key 的作用是什么?

答: key 帮助 React 识别哪些元素发生了变化,主要用于 Diff 算法的优化。有了 key,React 可以精确知道哪个元素被添加/删除/移动。

Q5:React 19 的新 JSX 运行时有啥变化?

答:

  • 不再依赖 React 对象
  • 使用 jsxjsxs 替代 React.createElement
  • 不需要手动 import React(在某些场景下)
  • 性能更好

📝 今日总结

概念老大爷版程序员版
JSX写的信语法糖
createElement快递单生成器核心转换函数
React Element建筑蓝图描述 UI 的对象
Fragment隐形文件夹不产生 DOM 节点的容器
key门牌号Diff 算法的唯一标识

🎯 今日自测

  1. JSX 转换后变成什么函数调用?
  2. React Element 包含哪些核心属性?
  3. 为什么不能用 index 作为 key?
  4. jsx 和 jsxs 的区别是什么?
  5. Fragment 的作用是什么?

📅 明日预告

Day 3:Hooks 原理

本日概念明日进阶
createElementuseState 的实现
React Element链表结构
静态分析Hooks 的调用规则

下节预告:React 是怎么记住你的 state 的?Hooks 底层原理大揭秘!🚀