Day 2:JSX 转换原理

📅 Day 2:JSX 转换原理

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


👴 老大爷能听懂版

JSX 是个啥?

想象你要寄一封信:

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

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


JSX 是怎么变成 createElement 的?

举个例子:

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

编译器帮你转换成:

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

就像这样:

css 复制代码
你写的 JSX:  <div>Hello</div>
                 ↓ 翻译
createElement:  createElement('div', null, 'Hello')

为什么要转来转去?

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

createElement 返回啥?

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

javascript 复制代码
// 这就是 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 是啥?

相当于"隐形文件夹":

jsx 复制代码
// 你写的
<>
  <div>A</div>
  <div>B</div>
</>

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

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


💻 专业开发者版

JSX 转换流程

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

React 19 的 JSX 运行时

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

javascript 复制代码
// 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.js jsx-runtime 导出
packages/react/jsx-runtime.js 包级别的 jsx-runtime 入口

createElement 源码解析

javascript 复制代码
// 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 源码

javascript 复制代码
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 手动调用 总是处理
javascript 复制代码
// 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 原理

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

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

相关推荐
我命由我123452 小时前
JS 开发问题:url.includes is not a function
开发语言·前端·javascript·html·ecmascript·html5·js
weixin199701080162 小时前
义乌购商品详情页前端性能优化实战
前端·性能优化
汪啊汪2 小时前
Day 3:Hooks 原理
前端
学以智用2 小时前
Vue3 + Vue Router 4 完整示例(可直接运行)
前端·vue.js
程序员小李白2 小时前
vue2基本语法详细解析(2.7条件渲染)
开发语言·前端·javascript
SuperEugene2 小时前
Vue3 项目目录结构规范:按业务域划分,新人快速上手|项目规范篇
前端·javascript·vue.js
悟空瞎说2 小时前
# 10年前端血坑:Canvas drawImage画不出图?90%的人栽在这几步
前端
qibmz2 小时前
新电脑安装 nvm 卡住?无需修改配置文件,一行命令完美解决!
前端
遗憾随她而去.2 小时前
高德地图自定义点标记: SVG vs HTML+CSS两种方案
前端·css