WinddSnow

React-Elements-and-JSX-createElement-Virtual-DOM-Fiber

字数统计: 3.9k阅读时长: 15 min
2026/07/31

第87课:React 元素与 JSX——createElement、JSX 转换、虚拟 DOM、Fiber 架构简介

React 的核心哲学是声明式 UI:你描述界面应该是什么样子,React 负责高效地更新 DOM 以匹配你的描述。理解这一哲学的关键,在于理解 React 的元素JSX虚拟 DOM 这三者的关系。JSX 是编写 React 元素最直观的语法,但它并非直接运行在浏览器中的代码——编译工具(如 Babel)会将 JSX 转换为 React.createElement(或 React 17+ 的 jsx 函数)调用,生成轻量的 JavaScript 对象——这就是虚拟 DOM。React 的 Fiber 架构则进一步将虚拟 DOM 的更新过程分解为可中断的异步任务,实现了并发渲染。本节课将逐一拆解 JSX 的编译产物、createElementjsx 函数的底层签名、虚拟 DOM 对象的数据结构以及 Fiber 架构的设计动机。


1. JSX:JavaScript 的语法扩展

JSX(JavaScript XML)是一种类似 HTML 的语法扩展,用于在 JavaScript 中描述 UI 结构。它不是模板语言,也不是 HTML——它的本质是函数调用的语法糖

1
const element = <h1 className="greeting">Hello, world!</h1>;

这段 JSX 在构建时会被 Babel(或其他编译工具)转换为普通的 JavaScript 函数调用。React 17 之前,转换结果为 React.createElement;React 17 引入了新的 JSX 转换,使用 jsxjsxDEV 函数,无需在每个文件中手动引入 React

1.1 JSX 表达式

JSX 中通过大括号 {} 嵌入任意 JavaScript 表达式。表达式在渲染时求值,结果被插入到对应位置。

1
2
3
4
5
6
7
8
9
10
const name = 'Alice';
const greeting = <p>Hello, {name}!</p>;

const list = (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);

JSX 本身也是表达式:编译后,JSX 标签变成函数调用,返回一个 JavaScript 对象。因此 JSX 可以赋值给变量、作为函数参数传递、作为函数返回值。

1.2 JSX 与 HTML 的差异

JSX 在属性命名上遵循 JavaScript 的 DOM API 规范(camelCase 命名),而非 HTML 属性名。

HTML 属性 JSX 属性 说明
class className 避免与 JavaScript class 关键字冲突。
for htmlFor for 是 JavaScript 保留字。
tabindex tabIndex camelCase 命名。
onclick onClick 事件处理采用 camelCase。
style="color:red" style={{ color: 'red' }} 接收 JavaScript 对象。
1
2
3
4
5
// ✅ JSX 正确写法
<div className="container" tabIndex={0} onClick={handleClick}>
<label htmlFor="username">用户名</label>
<input id="username" style={{ borderColor: 'blue' }} />
</div>

1.3 JSX 的编译产物

Babel 将 JSX 编译为 React.createElement(经典转换)或 jsx 函数(新转换)调用。React 17+ 推荐使用新的 JSX 转换,它自动从 react/jsx-runtime 导入 jsx 函数,无需手动 import React

1
2
3
4
5
6
7
8
9
10
// 源代码
const element = <h1 id="title">Hello</h1>;

// 经典转换(React 16 及之前)
import React from 'react';
const element = React.createElement('h1', { id: 'title' }, 'Hello');

// 新转换(React 17+)
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { id: 'title', children: 'Hello' });

2. createElement 与虚拟 DOM

2.1 React.createElement 签名

React.createElement(type, props, ...children) 接收三个参数:

  • **type**:元素类型。可以是 HTML 标签名字符串('div''span'),也可以是 React 组件(函数组件或类组件)。
  • **props**:包含所有传递给元素的属性(包括 children)。为 null 或对象。
  • **children**:零个或多个子元素。可以是字符串、数字、React 元素,或它们的数组。
1
2
3
4
5
6
// JSX:<div className="card">Click me</div>
const element = React.createElement(
'div',
{ className: 'card' },
'Click me'
);

2.2 虚拟 DOM 的数据结构

React.createElement 的返回值是一个普通的 JavaScript 对象,称为 React 元素(React Element)。它是对真实 DOM 节点的轻量描述。每个 React 元素大致包含以下属性:

1
2
3
4
5
6
7
8
9
10
11
const element = {
$$typeof: Symbol.for('react.element'), // 标识这是一个 React 元素
type: 'h1', // DOM 标签或组件函数
key: null, // 用于列表中的稳定标识
ref: null, // 用于获取真实 DOM 引用
props: {
id: 'title',
children: 'Hello' // 子元素(文本、其他元素数组)
},
_owner: null, // 内部使用,指向创建此元素的组件
};

关键属性

  • $$typeof: Symbol.for('react.element') 是 React 的安全机制——防止 XSS 攻击。因为 Symbol 无法被 JSON 序列化,即使恶意用户尝试通过 JSON 注入 React 元素,序列化后也会丢失 $$typeof,React 不会将其视为合法元素。
  • type 决定了渲染器如何处理该元素:如果是字符串(如 'div'),React 直接创建对应的 DOM 节点;如果是函数或类,React 调用该组件并递归渲染其返回值。
  • props.children 是递归树结构——每个子元素本身也是一个 React 元素对象,形成虚拟 DOM 树

虚拟 DOM 的核心价值:它是真实 DOM 的内存镜像。当状态变化时,React 在内存中生成新的虚拟 DOM 树,与旧的虚拟 DOM 树进行差异比较(Diffing),计算出最小的 DOM 操作集合,然后批量更新真实 DOM。这避免了直接频繁操作真实 DOM 带来的性能损耗。


3. 新 JSX 转换:jsxjsxs

React 17 引入了与 Babel 协同工作的新 JSX 转换,带来两个改进:

  • 无需手动导入 React:编译器自动从 react/jsx-runtime 引入 jsx 函数,JSX 文件中不再需要 import React from 'react'
  • 性能优化:新转换对静态子元素和动态子元素做了区分(jsx vs jsxs),减少了不必要的 children 数组创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 多个子元素时使用 jsxs
const element = (
<div>
<p>Static text</p>
{dynamicContent}
</div>
);

// 编译为:
import { jsxs as _jsxs } from 'react/jsx-runtime';
const element = _jsxs('div', {
children: [
_jsx('p', { children: 'Static text' }),
dynamicContent,
]
});

3.1 如何启用新 JSX 转换

在 Babel 配置中使用 @babel/preset-reactruntime: 'automatic' 选项:

1
2
3
4
5
6
// babel.config.js
module.exports = {
presets: [
['@babel/preset-react', { runtime: 'automatic' }]
]
};

TypeScript 编译器(tsconfig.json)中设置 "jsx": "react-jsx" 以启用新转换:

1
2
3
4
5
{
"compilerOptions": {
"jsx": "react-jsx"
}
}

4. 从虚拟 DOM 到 Fiber 架构

虚拟 DOM 的协调(Reconciliation) 算法负责比较新旧两棵虚拟 DOM 树并计算更新。在 React 15 及之前,这一过程是同步递归的——一旦开始渲染整棵树,就必须一气呵成,无法中断。对于大型应用,这会导致主线程被长时间占用,浏览器无法响应用户输入和动画,造成卡顿。

React 16 引入了全新的 Fiber 架构,重新设计了协调算法。Fiber 的核心思想是将渲染工作分解为小的工作单元,并允许调度器暂停、恢复和丢弃这些工作单元,基于优先级安排它们的执行。

4.1 Fiber 节点

在 Fiber 架构中,每个 React 元素对应一个 Fiber 节点。Fiber 节点不仅是元素数据的容器,还是一个链表结构,连接着父、子和兄弟节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
type: 'div',
key: null,
props: { children: [...] },

// Fiber 链表
return: parentFiber, // 父 Fiber
child: firstChildFiber, // 第一个子 Fiber
sibling: nextSiblingFiber, // 下一个兄弟 Fiber

// 副作用
effectTag: 'UPDATE', // 标记需要执行的 DOM 操作
nextEffect: nextEffectFiber, // 副作用链表
}

通过 childsiblingreturn 的链表结构,Fiber 可以增量式地遍历整个组件树,并在任意节点中断和恢复遍历。

4.2 双缓冲(Double Buffering)

Fiber 架构使用双缓冲技术:屏幕上当前显示的 UI 对应 current Fiber 树,正在内存中构建的新 UI 对应 workInProgress Fiber 树。两棵树通过 alternate 属性互相关联。构建完成后,React 将两棵树互换(提交阶段),workInProgress 变为新的 current。

4.3 工作循环:Render 阶段与 Commit 阶段

Fiber 架构将更新过程分为两个阶段:

  • Render 阶段(可中断):React 从根节点开始遍历 Fiber 树,为每个节点执行 beginWork(对比新旧节点)和 completeWork(创建 DOM 更新标记)。该阶段可以随时暂停,让出主线程给更高优先级的任务(如用户输入)。
  • Commit 阶段(不可中断):React 将 Render 阶段计算出的所有 DOM 变更一次性应用到真实 DOM。此阶段同步执行,不可中断,确保 UI 的一致性。

这种分阶段、可中断的架构为 并发模式(Concurrent Mode) 铺平了道路——React 可以根据任务优先级动态调整渲染顺序,保持应用流畅。


5. 元素、组件与实例的关系

React 中这三个概念经常被混淆:

  • React 元素:描述 UI 的普通 JavaScript 对象({ type, props }),是虚拟 DOM 的基本单元。它轻量、不可变。
  • React 组件:接收 Props 并返回 React 元素的函数或类。组件是可复用的 UI 逻辑封装。
  • 组件实例:当 React 渲染一个类组件时,会创建该类的实例,并在实例上维护 state 和生命周期方法。函数组件没有实例,它们通过 Hooks 管理状态。

流程:你编写组件 → 组件返回 React 元素 → React 元素构成虚拟 DOM 树 → React 将虚拟 DOM 转化为真实 DOM。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) JSX 语法中的 className 属性在编译后对应什么?
    A. HTML 的 class 属性
    B. JavaScript 的 class 关键字
    C. React 组件的类名
    D. CSS 模块的标识符

  2. (单选) React 17 的新 JSX 转换与经典转换的主要区别是什么?
    A. 完全移除了对 JSX 的支持。
    B. 不再需要手动 import React,编译器自动引入 jsx 函数。
    C. JSX 不再编译为 JavaScript 函数调用。
    D. 强制使用 TypeScript 编写 JSX。

  3. (填空) React 元素的 $$typeof 属性用于标识该对象是一个合法的 React 元素,其值通常是 ______

  4. (多选) 关于 Fiber 架构的描述,哪些是正确的?
    A. Fiber 节点通过链表结构(child、sibling、return)相互连接。
    B. Render 阶段是可中断的,Commit 阶段是不可中断的。
    C. Fiber 架构使 React 能够在渲染过程中让出主线程。
    D. Fiber 架构完全替代了虚拟 DOM 机制。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要编写一个函数 createVNode(type, props, ...children),模拟 React 的 createElement 函数,返回一个简化的虚拟 DOM 对象。该对象应包含 typeprops(包含 children 数组)和 keyref 属性(从 props 中提取)。children 中如果是字符串或数字,应转为文本节点对象 { type: 'TEXT_ELEMENT', props: { nodeValue: value, children: [] } }。然后编写一个 render 函数,将该虚拟 DOM 树递归渲染为真实 DOM 并挂载到指定容器。无需处理更新和事件——仅实现首次渲染。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成包含 createVNoderender 函数的完整 JavaScript 代码。提示词中需明确指定虚拟 DOM 对象的形状、文本节点的处理方式和渲染逻辑。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个简化的 Virtual DOM 引擎,模拟 React 的核心概念。需要创建以下文件:

  1. src/vdom.js:导出两个函数:
    • createVNode(type, props, ...children):返回一个虚拟 DOM 对象。如果 type 是函数,直接调用它并传入 props(简化处理,不实现完整的组件模型)。从 props 中提取 keyref,其余属性保留。children 展平后,对每个子节点:如果是 stringnumber,转为 { type: 'TEXT_ELEMENT', props: { nodeValue: String(child), children: [] } };如果已经是对象则保留。
    • render(vNode, container):递归地将虚拟 DOM 渲染为真实 DOM。对 TEXT_ELEMENT 创建文本节点;对普通元素创建对应标签的 DOM 节点,设置属性(忽略 childrenkeyref),递归渲染子节点后 container.appendChild。最终替换容器内容。
  2. src/index.js:使用 createVNode 构建一个简单的 DOM 结构(如包含 h1 标题和 p 段落的 div),调用 render 挂载到 #app
  3. 添加 JSDoc 注释,并在代码中注释解释每个步骤与 React createElement 和虚拟 DOM 的对应关系。
    完成后列出所有文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请解释 React 中虚拟 DOM 的概念及其工作原理。它如何与真实 DOM 保持同步?Fiber 架构相比传统虚拟 DOM 的栈协调算法有什么突破性改进?什么是“可中断渲染”,它解决了什么问题?

参考答案

虚拟 DOM 是真实 DOM 的轻量 JavaScript 对象表示。React 使用 React.createElement(或 JSX 编译产物)生成虚拟 DOM 树。当状态变化时,React 生成新的虚拟 DOM 树,通过协调算法(Reconciliation) 比较新旧两棵树的差异,计算出最小 DOM 操作集合,然后批量更新真实 DOM。虚拟 DOM 的优势在于减少了直接操作真实 DOM 的频率(真实 DOM 操作昂贵),并将变更批量提交。

Fiber 架构的突破在于将渲染工作从不可中断的递归(栈协调)改为可中断的增量式链表遍历。传统栈协调同步递归整棵虚拟 DOM 树,一旦开始就无法停止,可能导致大型组件树的渲染长时间占用主线程,阻塞用户交互和动画。Fiber 将每个组件节点组织为 Fiber 节点,通过 childsiblingreturn 的链表结构遍历,使得遍历过程可以在任意节点暂停(让出主线程给浏览器处理高优先级事件),并在之后恢复。Render 阶段(可中断)负责计算差异,Commit 阶段(不可中断)批量应用 DOM 变更。

可中断渲染 是 Fiber 架构支持并发模式的基石,它解决了大型应用中渲染阻塞用户交互的问题,使 React 能够根据任务优先级动态调整渲染顺序,保持 60fps 的流畅体验。


课后练习答案

一、概念自测答案

  1. A

    • 解析:className 在编译后映射到 HTML 的 class 属性,因为 class 是 JavaScript 的保留字。
  2. B

    • 解析:React 17 新 JSX 转换由编译器自动引入 jsx 函数,无需手动 import React。C 错误(仍编译为函数调用);A、D 错误。
  3. Symbol.for('react.element')

    • 解析:React 元素通过 $$typeof: Symbol.for('react.element') 标识。
  4. A、B、C

    • 解析:A 正确,Fiber 通过 childsiblingreturn 形成链表;B 正确,Render 可中断,Commit 不可中断;C 正确,可中断特性使 React 能让出主线程;D 错误,Fiber 架构是虚拟 DOM 协调算法的重新实现,并未取代虚拟 DOM。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请实现一个简化的 Virtual DOM 引擎,包含 createVNoderender 两个函数。要求:

  • createVNode(type, props, ...children) 返回 { type, props: { ...props, children } },其中 children 递归处理:文本/数字转为 { type: 'TEXT_ELEMENT', props: { nodeValue: String(child), children: [] } },数组元素展平。
  • render(vNode, container):根据 type 创建 DOM 节点,文本节点用 document.createTextNode,普通元素用 document.createElement。遍历 props 设置属性(跳过 children)。递归渲染子节点后挂载到容器。
  • 在代码中注释说明每个步骤与 React createElement 和虚拟 DOM 的对应关系。
  • 提供使用示例,构建一个包含 h1 和 p 的 div 并渲染到页面。输出完整代码。”
CATALOG
  1. 1. 第87课:React 元素与 JSX——createElement、JSX 转换、虚拟 DOM、Fiber 架构简介
    1. 1.1. 1. JSX:JavaScript 的语法扩展
      1. 1.1.1. 1.1 JSX 表达式
      2. 1.1.2. 1.2 JSX 与 HTML 的差异
      3. 1.1.3. 1.3 JSX 的编译产物
    2. 1.2. 2. createElement 与虚拟 DOM
      1. 1.2.1. 2.1 React.createElement 签名
      2. 1.2.2. 2.2 虚拟 DOM 的数据结构
    3. 1.3. 3. 新 JSX 转换:jsx 与 jsxs
      1. 1.3.1. 3.1 如何启用新 JSX 转换
    4. 1.4. 4. 从虚拟 DOM 到 Fiber 架构
      1. 1.4.1. 4.1 Fiber 节点
      2. 1.4.2. 4.2 双缓冲(Double Buffering)
      3. 1.4.3. 4.3 工作循环:Render 阶段与 Commit 阶段
    5. 1.5. 5. 元素、组件与实例的关系
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)