📅 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')
为什么要转来转去?
- 浏览器不认识 JSX --- 浏览器只懂原生 JavaScript,需要转换
- 统一入口 --- 无论是手写还是自动生成,最后都调用同一个函数
- 描述 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对象- 使用
jsx和jsxs替代React.createElement- 不需要手动 import React(在某些场景下)
- 性能更好
📝 今日总结
| 概念 | 老大爷版 | 程序员版 |
|---|---|---|
| JSX | 写的信 | 语法糖 |
| createElement | 快递单生成器 | 核心转换函数 |
| React Element | 建筑蓝图 | 描述 UI 的对象 |
| Fragment | 隐形文件夹 | 不产生 DOM 节点的容器 |
| key | 门牌号 | Diff 算法的唯一标识 |
🎯 今日自测
- JSX 转换后变成什么函数调用?
- React Element 包含哪些核心属性?
- 为什么不能用 index 作为 key?
- jsx 和 jsxs 的区别是什么?
- Fragment 的作用是什么?
📅 明日预告
Day 3:Hooks 原理
| 本日概念 | 明日进阶 |
|---|---|
| createElement | useState 的实现 |
| React Element | 链表结构 |
| 静态分析 | Hooks 的调用规则 |
下节预告:React 是怎么记住你的 state 的?Hooks 底层原理大揭秘!🚀