说说 React JSX 转换成真实 DOM 的过程?
一、是什么
JSX(JavaScript XML)是 React 提供的语法扩展,允许开发者在 JavaScript 中编写类似 HTML 的标记。JSX 不是合法的 JavaScript,浏览器无法直接执行,需要通过编译工具转换为标准的 JavaScript 函数调用。
从 JSX 到屏幕上的真实 DOM,经历了一条完整的流水线:
JSX 源码
→ Babel 编译为 JS 函数调用
→ 执行函数调用生成 ReactElement(虚拟 DOM)
→ React Render Phase(构建 Fiber 树)
→ React Commit Phase(操作真实 DOM)
→ 浏览器渲染到屏幕
二、JSX 的编译转换
2.1 经典模式(React 17 之前)
在 React 17 之前,JSX 统一编译为 React.createElement 调用,因此每个使用 JSX 的文件都必须导入 React:
// 源码
import React from "react";
function App() {
return (
<div className="app">
<h1>标题</h1>
<p>段落内容</p>
</div>
);
}
// Babel 编译后
import React from "react";
function App() {
return React.createElement(
"div",
{ className: "app" },
React.createElement("h1", null, "标题"),
React.createElement("p", null, "段落内容")
);
}
2.2 自动模式(React 17+ Automatic Runtime)
React 17 引入了全新的 JSX Transform,Babel 会自动从 react/jsx-runtime 导入所需函数,开发者不再需要手动 import React:
// 源码(不需要 import React)
function App() {
return (
<div className="app">
<h1>标题</h1>
<p>段落内容</p>
</div>
);
}
// Babel 编译后
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function App() {
return _jsxs("div", {
className: "app",
children: [
_jsx("h1", { children: "标题" }),
_jsx("p", { children: "段落内容" }),
],
});
}
两种模式的区别:
jsxs 用于有多个子节点的情况,React 内部利用这个信息跳过不必要的 key 检查。
2.3 Babel 配置
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
在 TypeScript 项目中,tsconfig.json 需要设置:
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
三、ReactElement 的生成
无论使用哪种编译模式,最终都会生成 ReactElement 对象。它是虚拟 DOM 的基本单元:
// React.createElement 的简化实现
function createElement(type, config, ...children) {
const props = {};
let key = null;
let ref = null;
if (config != null) {
if (config.key !== undefined) {
key = "" + config.key;
}
if (config.ref !== undefined) {
ref = config.ref;
}
for (const propName in config) {
if (
Object.prototype.hasOwnProperty.call(config, propName) &&
propName !== "key" &&
propName !== "ref"
) {
props[propName] = config[propName];
}
}
}
if (children.length === 1) {
props.children = children[0];
} else if (children.length > 1) {
props.children = children;
}
return {
$$typeof: Symbol.for("react.element"),
type,
key,
ref,
props,
};
}
type 的值有以下几种情况:
- 字符串:原生 HTML 标签,如
"div"、"span"
- 函数:函数组件,如
App
- 类:类组件(类本质上也是函数)
- Symbol:React 内置类型,如
Fragment、Suspense、StrictMode
// 不同 type 的例子
<div /> // type: "div"
<App /> // type: App(函数引用)
<React.Fragment> // type: Symbol(react.fragment)
<Suspense> // type: Symbol(react.suspense)
四、Render Phase — 构建 Fiber 树
ReactElement 只是静态的描述对象,React 还需要把它转换为 Fiber 节点,构建 Fiber 树来管理组件的状态、副作用和更新调度。
4.1 Fiber 节点结构
// Fiber 节点的简化结构
const fiber = {
tag: 5, // HostComponent(原生DOM元素)
type: "div",
key: null,
stateNode: null, // 对应的真实 DOM 节点(首次渲染时为 null)
return: parentFiber, // 父 Fiber
child: firstChildFiber, // 第一个子 Fiber
sibling: nextSiblingFiber, // 下一个兄弟 Fiber
pendingProps: { className: "app", children: [...] },
memoizedProps: null, // 上次渲染的 props
memoizedState: null, // 上次渲染的 state(Hooks 链表)
flags: 0, // 副作用标记(Placement, Update, Deletion 等)
updateQueue: null,
};
Fiber 树使用链表结构而非传统树结构,每个节点通过 child、sibling、return 三个指针连接。这种设计使得遍历可以随时中断和恢复。
4.2 双缓冲(Double Buffering)
React 维护两棵 Fiber 树:
- current 树:当前屏幕上显示的 UI 对应的 Fiber 树
- workInProgress 树:正在构建的新 Fiber 树
current 树 workInProgress 树
(屏幕显示) (后台构建)
FiberRoot FiberRoot
| |
App Fiber ←alternate→ App Fiber (wip)
| |
div Fiber ←alternate→ div Fiber (wip)
/ \ / \
h1 p ←alternate→ h1 p (wip)
当 workInProgress 树构建完成后,React 通过交换指针将其变为 current 树,实现快速切换。
4.3 Render Phase 的工作
Render Phase(也叫 Reconciliation)的核心工作是递归遍历 ReactElement 树,对比新旧 Fiber,标记需要的 DOM 操作:
beginWork(向下递归)
↓ 处理当前节点,创建子 Fiber
↓ 标记副作用(Placement/Update/Deletion)
completeWork(向上回溯)
↓ 创建真实 DOM 实例(但不挂载)
↓ 收集副作用链表
整个过程是可中断的。React 使用调度器(Scheduler)控制执行节奏,在每一帧的空闲时间执行工作。如果有更高优先级的任务(用户输入等),当前工作会被暂停。
五、Commit Phase — 操作真实 DOM
Commit Phase 是同步不可中断的,分为三个子阶段:
5.1 Before Mutation
执行 getSnapshotBeforeUpdate(类组件)
调度 useEffect(异步,不在此阶段执行)
5.2 Mutation
这是真正操作 DOM 的阶段,根据 Fiber 节点的 flags 标记执行对应操作:
// 简化的 DOM 操作逻辑
function commitMutationEffects(fiber) {
switch (fiber.flags) {
case Placement:
// 新增节点
parentDOM.appendChild(fiber.stateNode);
break;
case Update:
// 更新属性
updateDOMProperties(fiber.stateNode, oldProps, newProps);
break;
case Deletion:
// 删除节点
parentDOM.removeChild(fiber.stateNode);
break;
case PlacementAndUpdate:
// 移动并更新
parentDOM.appendChild(fiber.stateNode);
updateDOMProperties(fiber.stateNode, oldProps, newProps);
break;
}
}
5.3 Layout
执行 componentDidMount / componentDidUpdate(类组件)
执行 useLayoutEffect 的回调(同步执行)
将 current 指针切换到 workInProgress 树
最后,浏览器在下一个渲染帧将变更绘制到屏幕上。useEffect 的回调会在浏览器绘制完成后异步执行。
六、完整流程总结
以首次渲染为例:
function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>计数: {count}</h1>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
</div>
);
}
createRoot(document.getElementById("root")).render(<App />);
执行流程:
1. JSX 编译
<App /> → jsx(App, {})
返回 ReactElement: { type: App, props: {} }
2. 创建 FiberRoot
createRoot 创建 FiberRootNode 和 HostRoot Fiber
3. Render Phase
a. 执行 App 函数,得到子 ReactElement 树
b. 为每个 ReactElement 创建 Fiber 节点
c. 初始化 useState → memoizedState = 0
d. 标记所有新节点为 Placement(需要插入)
e. completeWork 中创建真实 DOM 实例(div, h1, button)
但不挂载到页面
4. Commit Phase
a. Before Mutation:无操作
b. Mutation:将 DOM 树一次性插入到 #root 容器中
c. Layout:切换 current 树
5. 浏览器渲染
浏览器在下一帧将 DOM 绘制到屏幕
6. 异步回调
useEffect 回调在绘制后执行(本例无 useEffect)
更新时流程类似:触发 setState → 调度新的 Render Phase → 重新执行组件函数 → Diff 新旧 Fiber 树 → Commit Phase 只更新变化的 DOM 节点。理解这个流程有助于性能优化——在 Render Phase 通过 React.memo、useMemo 减少不必要的 Fiber 创建和比较,在 Commit Phase 通过减少 DOM 变更量提升渲染效率。