深入学习 React 源码实现之 JSX与虚拟DOM生成
📘 一、JSX 简介与历史演进
1.1 什么是 JSX?
JSX(JavaScript XML) 是一种 JavaScript 的语法扩展,允许开发者在 JavaScript 中直接书写类似 HTML 或 XML 的结构。
const element = <h1>Hello, world!</h1>;
1.2 JSX 的起源
- 诞生背景:Facebook 在开发 React 时发现传统的模板语言无法满足组件化开发需求。
- 首次亮相:React v0.12 版本中正式引入 JSX,并通过 Babel 插件
@babel/plugin-transform-react-jsx将其转换为标准的React.createElement()调用。 - 标准化尝试:目前 JSX 本身并不是 ECMAScript 标准的一部分,但已广泛被社区接受。
1.3 JSX 的本质
JSX 最终会被编译成 React.createElement(type, props, children) 函数调用:
// JSX
const element = <h1 className="greeting">Hello, world!</h1>;
// 编译后
const element = React.createElement(
'h1',
{ className: 'greeting' },
'Hello, world!'
);
📂 二、React 源码中的 JSX 实现路径
2.1 JSX 转换核心源码文件
| 文件路径 | 功能 |
|---|---|
packages/react/src/ReactElement.js | 定义 React.createElement() 方法 |
packages/babel-plugin-transform-react-jsx/ | Babel 插件将 JSX 转换为 React.createElement() 调用 |
2.2 核心函数解析:createElement()
这是一个简化版的 React createElement 函数实现,用于创建 React 元素。下面是每行代码与详细注释:
// packages/react/src/ReactElement.js
// 导出一个名为 createElement 的函数
export function createElement(type, config, children) {
// 声明一个变量用于存储属性名
let propName;
// 初始化一个空对象用于存储属性
const props = {};
// 声明两个变量用于处理 key 和 ref
let key = null;
let ref = null;
// 检查 config 是否存在(非 null 或 undefined)
if (config != null) {
// 检查 config 是否有有效的 ref 属性
if (hasValidRef(config)) {
// 如果有,将 ref 赋值给 ref 变量
ref = config.ref;
}
// 检查 config 是否有有效的 key 属性
if (hasValidKey(config)) {
// 如果有,将 key 转换为字符串并赋值给 key 变量
key = '' + config.key;
}
// 遍历 config 对象的所有属性
for (propName in config) {
// 检查属性是否是 config 对象自身的属性(非继承的)
// 并且不是保留属性(RESERVED_PROPS 中的属性)
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
// 将非保留属性复制到 props 对象中
props[propName] = config[propName];
}
}
}
// 计算子元素的数量(arguments.length - 2 因为 type 和 config 是前两个参数)
const childrenLength = arguments.length - 2;
// 处理只有一个子元素的情况
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
props.children = childArray;
}
// 返回一个 ReactElement 对象
return ReactElement(
type, // 元素类型(如 'div'、'span' 或自定义组件)
key, // 元素的 key(用于列表渲染时的标识)
ref, // 元素的 ref(用于访问 DOM 节点或组件实例)
undefined, // 通常用于内部使用(self)
undefined, // 通常用于内部使用(source)
ReactCurrentOwner.current, // 当前正在创建元素的组件
props // 元素的属性对象
);
}
老曹笔记🚀
- hasValidRef 和 hasValidKey 是假设存在的辅助函数,用于验证 ref 和 key 的有效性
- RESERVED_PROPS 是一个包含 React 保留属性名的对象(如 key、ref 等)
- ReactElement 是一个内部函数,用于创建最终的 React 元素对象
- ReactCurrentOwner.current 用于跟踪当前正在创建元素的组件(在 React 的上下文中)
- 这个实现是 React 16 之前版本的简化版本,现代 React 的实现更加复杂,包含了更多的优化和特性
2.3 ReactElement 构造函数
这是一个简化版的 ReactElement 工厂函数实现,用于创建 React 元素对象。下面是每行代码的详细注释:
// 定义一个名为 ReactElement 的函数,接收多个参数
const ReactElement = function(type, key, ref, self, source, owner, props) {
// 创建一个新的对象,表示 React 元素
const element = {
// 使用特殊符号 $$typeof 标记这是一个 React 元素
// REACT_ELEMENT_TYPE 是一个内部常量,用于唯一标识 React 元素类型
$$typeof: REACT_ELEMENT_TYPE,
type: type,// 元素的类型,可以是原生 DOM 标签名(如 'div')或自定义组件
key: key, // 元素的 key,用于在列表渲染时帮助 React 识别哪些元素发生了变化
ref: ref,// 元素的 ref,用于访问 DOM 节点或组件实例
props: props, // 元素的属性对象,包含传递给组件的所有 props
_owner: owner, // 指向创建该元素的组件(通常用于开发工具和错误信息)
};
// 返回创建的 React 元素对象
return element;
};
补充说明:
REACT_ELEMENT_TYPE是一个内部常量,用于唯一标识 React 元素类型,通常是一个 Symbol(如Symbol.for('react.element'))self和source参数在这个简化版本中没有被使用,但在完整的 React 实现中,它们可能用于开发目的(如调试信息)_owner属性通常指向创建该元素的组件,这在 React 开发工具中用于显示组件层次结构- 这个函数创建的对象是一个普通的 JavaScript 对象,不是真正的 DOM 元素
- React 使用这种结构来描述 UI 的虚拟表示(虚拟 DOM),然后由 React 的 reconciliation 算法决定如何更新实际 DOM
🔍 三、设计模式分析
3.1 工厂模式(Factory Pattern)
React.createElement()是一个典型的工厂方法,封装了创建 React 元素的逻辑。- 用户无需关心底层构造细节,只需传入类型和属性即可。
3.2 抽象语法树(AST)构建思想
- JSX 经过 Babel 解析后生成 AST;
- 再由 Babel 插件将其转换为
React.createElement()调用; - 这种“编译器+运行时”的方式体现了前端工程化的抽象思维。
3.3 单例模式(Singleton)
ReactElement创建的对象是不可变的(Immutable),保证了渲染过程的一致性。
💡 四、完整 JSX 实现代码示例(Mini-React)
我们来手动实现一个简化版的 JSX 到 Virtual DOM 的转换器。
4.1 自定义 createElement
// 定义一个名为 createElement 的函数,接收三个参数:
// type - 元素类型(如 'div' 或自定义组件)
// props - 元素的属性对象
// ...children - 剩余参数,表示子元素
function createElement(type, props, ...children) {
// 返回一个对象,表示 React 元素
return {
// 元素类型
type,
// 元素属性对象
props: {
// 展开传入的 props 对象
...props,
// 处理子元素
children: children.map(child =>
// 检查子元素是否是对象(即是否是 React 元素)
typeof child === "object"
? child
// 如果不是对象,则创建文本元素
: createTextElement(child)
),
},
};
}
// 定义一个名为 createTextElement 的函数,用于创建文本元素
function createTextElement(text) {
// 返回一个对象,表示文本元素
return {
// 固定类型为 "TEXT_ELEMENT",用于区分普通元素
type: "TEXT_ELEMENT",
// 文本元素的属性对象
props: {
// 存储文本内容
nodeValue: text,
// 文本元素没有子元素,所以设为空数组
children: [],
},
};
}
4.2 渲染函数
算法思路
- DOM 节点创建: 根据 element.type 创建相应的 DOM 节点。 如果是
“TEXT_ELEMENT”,则创建一个文本节点。 否则,创建一个普通 DOM 元素(如 div、span 等)。- 属性处理: 使用 Object.keys(element.props) 获取元素的所有属性名。 通过
filter(isProperty) 过滤掉 “children” 属性,因为 “children” 是子元素,不是 DOM 属性。
遍历剩余的属性名,并将属性值赋给 DOM 节点。- 子元素渲染: 遍历 element.props.children,递归调用 render 函数渲染每个子元素。 子元素会被附加到当前
DOM 节点中。- 附加到容器: 最后,将渲染好的 DOM 节点附加到传入的 container 节点中。
// 定义一个名为 render 的函数,接收两个参数:
// element - 要渲染的 React 元素
// container - 容器 DOM 节点,渲染结果将被附加到这个节点中
function render(element, container) {
// 创建一个 DOM 节点
const dom =
// 检查元素类型是否是 "TEXT_ELEMENT"(文本元素)
element.type === "TEXT_ELEMENT"
? // 如果是,创建一个文本节点
document.createTextNode("")
: // 否则,创建一个普通 DOM 元素
document.createElement(element.type);
// 定义一个辅助函数,用于判断属性名是否是有效的 DOM 属性(排除 "children")
const isProperty = key => key !== "children";
// 获取元素的所有属性名,过滤掉 "children" 属性
Object.keys(element.props)
.filter(isProperty)
// 遍历剩余的属性名
.forEach(name => {
// 将属性值赋给 DOM 节点
dom[name] = element.props[name];
});
// 遍历所有子元素
element.props.children.forEach(child => {
// 递归渲染每个子元素,并将其附加到当前 DOM 节点
render(child, dom);
});
// 将渲染好的 DOM 节点附加到容器节点中
container.appendChild(dom);
}
4.3 使用示例
/** @jsx createElement */
const element = (
<div id="foo">
<a href="#">Click me</a>
<b>Hello</b>
</div>
);
render(element, document.getElementById("root"));
❓ 五、10大 JSX 高频面试题
下面是对 10 大 JSX 高频面试题 的详细讲解,涵盖每个问题的核心知识点和实际应用技巧,适合准备react前端面试的同学深入理解。
1. JSX 是什么?它是如何工作的?
JSX(JavaScript XML) 是一种 JavaScript 的语法扩展,允许开发者在 JS 文件中像写 HTML 一样编写结构代码。
const element = <h1>Hello, world!</h1>;
实际上,JSX 并不是标准的 JavaScript 语法,它需要通过 Babel 插件(如 @babel/plugin-transform-react-jsx) 转换为合法的 JS 表达式:
const element = React.createElement('h1', null, 'Hello, world!');
✅ 核心点:JSX 是语法糖,最终会被编译成
React.createElement()函数调用。
2. React.createElement() 有什么参数?
React.createElement() 是 React 构建 Virtual DOM 的核心函数。它的基本签名如下:
React.createElement(type, [props], [...children])
type: 元素类型,可以是字符串(如'div')、组件类(如MyComponent),也可以是 Fragment。props: 属性对象,包含元素的所有属性(如className,style, 自定义属性等)。children: 子元素,可以是一个或多个节点,也可以是文本、数字等。
示例:
<div className="box" style={{ color: 'red' }}>Hello</div>
转换为:
React.createElement('div', { className: 'box', style: { color: 'red' } }, 'Hello');
✅ 核心点:掌握
createElement的参数结构有助于理解 React 的渲染机制。
3. 为什么不能使用 if 语句在 JSX 中?
JSX 是一个表达式(expression),而不是语句(statement)。因此你不能直接在 JSX 中使用如 if、for 这样的控制流语句。
错误示例:
{
if (condition) {
<p>显示内容</p>
}
}
但你可以使用三元运算符或立即执行函数来实现逻辑判断:
✅ 正确做法:
{ condition ? <p>显示内容</p> : null }
// 或者使用 IIFE
{
(() => {
if (condition) return <p>显示内容</p>;
return null;
})()
}
✅ 核心点:JSX 中只能嵌入表达式,不能直接写语句。
4. 如何在 JSX 中使用变量?
在 JSX 中,你可以通过 {} 插入任何合法的 JavaScript 表达式,包括变量、函数调用、数组等。
示例:
const name = 'Alice';
<p>Hello, {name}</p>
// 使用数组 map 渲染列表
const items = ['Apple', 'Banana', 'Orange'];
<ul>{items.map(item => <li key={item}>{item}</li>)}</ul>
✅ 核心点:JSX 中的
{}可以插入任意合法 JS 表达式,但不能直接写语句。
5. JSX 中如何绑定事件?
在 JSX 中绑定事件时,必须使用 驼峰命名法,并且传入的是一个函数引用,而不是字符串。
正确示例:
<button onClick={handleClick}>点击我</button>
注意不要加括号,否则会立即执行函数:
❌ 错误写法:
<button onClick={handleClick()}>点击我</button> // 立即执行了 handleClick
如果你需要传参,可以用箭头函数包裹:
<button onClick={() => handleClick(id)}>点击我</button>
✅ 核心点:事件名使用驼峰命名,传入函数引用而非调用。
6. JSX 如何渲染列表?
通常我们使用 .map() 方法将数组映射为一组 JSX 元素,并返回给 JSX 渲染。
示例:
const list = ['Item 1', 'Item 2', 'Item 3'];
<ul>
{list.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
⚠️ 注意:
- 每个列表项必须有唯一的
key属性; - 不要使用索引作为
key,除非数据是静态且不会变化。
7. key 的作用是什么?
key 是 React 用来识别哪些元素发生了变化、被添加或删除的关键标识符。
当你在 .map() 中渲染列表时,React 会根据 key 来决定是否复用已有 DOM 节点,从而提升性能。
示例:
{items.map(item => <TodoItem key={item.id} item={item} />)}
✅ 核心点:
key帮助 React 高效地更新虚拟 DOM;- 推荐使用唯一 ID 作为
key,而不是索引; - 如果没有唯一 ID,可以考虑使用
uuid或其他生成方式。
8. 如何在 JSX 中写注释?
JSX 中不能使用 HTML 注释 <!-- -->,也不能使用单行注释 //,因为它们都不是合法的 JSX 表达式。
正确写法是使用 {/* ... */} 包裹注释内容:
{/* 这是一个注释 */}
<div>正常内容</div>
多行注释也适用:
{/*
这是
多行
注释
*/}
✅ 核心点:在 JSX 中使用
{/* ... */}编写注释。
9. Fragment 有什么作用?
Fragment 是 React 提供的一个特殊组件,用于包裹多个 JSX 元素而不引入额外的 DOM 节点。
通常我们这样写:
return (
<div>
<h1>Title</h1>
<p>Paragraph</p>
</div>
);
但如果我们不想添加额外的 <div>,可以使用 Fragment:
import React from 'react';
return (
<React.Fragment>
<h1>Title</h1>
<p>Paragraph</p>
</React.Fragment>
);
还可以简写为:
<>
<h1>Title</h1>
<p>Paragraph</p>
</>
✅ 核心点:
Fragment可以避免额外的 DOM 节点,常用于组件返回多个根元素。
10. 如何避免 XSS 攻击?
React 在默认情况下会对所有插入到 JSX 中的内容进行自动转义,防止跨站脚本攻击(XSS)。
例如:
const userInput = '<script>alert("XSS")</script>';
<div>{userInput}</div>
这段代码不会执行恶意脚本,而是原样输出为文本。
但如果使用 dangerouslySetInnerHTML 强制插入 HTML,则需要特别小心:
<div dangerouslySetInnerHTML={{ __html: userInput }} />
✅ 核心点:
- React 默认对内容进行转义,防止 XSS;
- 尽量避免使用
dangerouslySetInnerHTML; - 如果必须使用,确保内容是可信来源并经过清理。
🎁 六、总结与推荐资源
✅ 下一步学习路径
- 先掌握 JSX 的基本语法;
- 理解 JSX →
React.createElement()的转换过程; - 阅读 React 源码中
ReactElement.js的实现; - 尝试手写一个简易 JSX 到 Virtual DOM 的转换器;
- 学习 Babel 插件开发,深入理解 AST;
- 最终目标:阅读 React Fiber 架构源码,理解整个渲染流程。
📚 推荐参考资料
| 类型 | 名称 | 地址 |
|---|---|---|
| 文档 | React 官方文档 | https://react.dev |
| GitHub | Build Your Own React | https://github.com/pomber/build-your-own-react |
| 视频 | Lin Clark - React Conf 2017 | YouTube - React Conf 2017 |
| 社区 | React China | https://react-china.org/ |
| 工具 | Babel Playground | https://babeljs.io/repl |

2558

被折叠的 条评论
为什么被折叠?



