说说 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: "段落内容" }),
    ],
  });
}

两种模式的区别:

特性Classic(经典)Automatic(自动)
函数React.createElementjsx / jsxs
导入需手动 import React编译器自动注入
children 传递作为第 3+ 个参数作为 props.children
单子节点 / 多子节点同一个函数jsx / jsxs 区分

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 内置类型,如 FragmentSuspenseStrictMode
// 不同 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 树使用链表结构而非传统树结构,每个节点通过 childsiblingreturn 三个指针连接。这种设计使得遍历可以随时中断和恢复。

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.memouseMemo 减少不必要的 Fiber 创建和比较,在 Commit Phase 通过减少 DOM 变更量提升渲染效率。