Skip to content

JSX 与虚拟 DOM

React 的基石:声明式 UI 与高效更新

1. JSX 本质

1.1 JSX 是语法糖

JSX 不是模板语言,而是 React.createElement语法糖

jsx
// JSX 写法
const element = (
    <div className="container">
        <h1>Hello</h1>
        <p>World</p>
    </div>
);

// 编译后 (React 17+)
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';

const element = _jsxs("div", {
    className: "container",
    children: [
        _jsx("h1", { children: "Hello" }),
        _jsx("p", { children: "World" })
    ]
});

1.2 createElement 签名

javascript
// React 16 及之前
React.createElement(
    type,       // 'div' | Component
    props,      // { className: 'foo', onClick: fn }
    ...children // 子元素
);

// React 17+ 的 jsx-runtime
jsx(type, { ...props, children });
jsxs(type, { ...props, children: [] });  // 多个 children

1.3 编译配置

javascript
// babel.config.js
module.exports = {
    presets: [
        ['@babel/preset-react', {
            runtime: 'automatic',  // React 17+ 自动引入
            // runtime: 'classic'   // 需要手动 import React
        }]
    ]
};

2. 虚拟 DOM

2.1 什么是虚拟 DOM?

虚拟 DOM 是真实 DOM 的 JS 对象表示

jsx
// JSX
<div id="app" className="container">
    <span>Hello</span>
</div>

// 虚拟 DOM 对象
{
    type: 'div',
    props: {
        id: 'app',
        className: 'container',
        children: [{
            type: 'span',
            props: {
                children: 'Hello'
            }
        }]
    }
}

2.2 ReactElement 结构

javascript
// 简化的 ReactElement 结构
const element = {
    $$typeof: Symbol.for('react.element'),  // 安全标识
    type: 'div',                             // 标签名或组件
    key: null,                               // diff 用的 key
    ref: null,                               // ref 引用
    props: {                                 // 属性
        className: 'app',
        children: [...]
    },
    _owner: null,                            // 创建它的组件
};

2.3 为什么需要虚拟 DOM?

┌─────────────────────────────────────────────────┐
│              虚拟 DOM 的价值                     │
├─────────────────────────────────────────────────┤
│                                                 │
│  1. 声明式编程                                  │
│     描述"UI 应该是什么样"                        │
│     而不是"如何操作 DOM"                         │
│                                                 │
│  2. 批量更新                                    │
│     多次 setState → 一次 DOM 更新               │
│                                                 │
│  3. 跨平台                                      │
│     同一份虚拟 DOM 可渲染到:                    │
│     - 浏览器 (react-dom)                        │
│     - 移动端 (react-native)                     │
│     - 服务端 (renderToString)                   │
│                                                 │
│  4. Diff 优化                                   │
│     只更新变化的部分,最小化 DOM 操作            │
│                                                 │
└─────────────────────────────────────────────────┘

3. 渲染流程

3.1 首次渲染

javascript
// 入口
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);
┌─────────────────────────────────────────────────┐
│              首次渲染流程                         │
├─────────────────────────────────────────────────┤
│                                                 │
│  <App />                                        │
│     │                                           │
│     ▼                                           │
│  React.createElement(App, null)                 │
│     │                                           │
│     ▼                                           │
│  创建 Fiber 树                                  │
│     │                                           │
│     ▼                                           │
│  执行组件函数,生成 ReactElement                 │
│     │                                           │
│     ▼                                           │
│  创建真实 DOM 节点                               │
│     │                                           │
│     ▼                                           │
│  插入到容器中                                    │
│                                                 │
└─────────────────────────────────────────────────┘

3.2 更新渲染

setState() / props 变化


   调度更新 (Scheduler)


┌───────────────────┐
│   Render 阶段      │  ← 可中断
│   (构建 Fiber 树)  │
└───────────────────┘


    Diff 算法
    标记 Effect


┌───────────────────┐
│   Commit 阶段      │  ← 不可中断
│   (更新 DOM)       │
└───────────────────┘

4. key 的作用

4.1 为什么需要 key?

jsx
// 列表渲染
{items.map(item => (
    <li key={item.id}>{item.name}</li>
))}

key 帮助 React 识别哪些元素改变了

旧列表: [A, B, C]  key: [1, 2, 3]
新列表: [A, C]     key: [1, 3]

有 key: React 知道删除了 key=2 的 B
无 key: React 只能按顺序对比,可能导致错误更新

4.2 key 的错误用法

jsx
// ❌ 错误:用 index 作为 key
{items.map((item, index) => (
    <li key={index}>{item.name}</li>
))}

// 问题场景:删除第一个元素
// 旧: [A(key=0), B(key=1), C(key=2)]
// 新: [B(key=0), C(key=1)]
// React 认为: 0→0复用, 1→1复用, 删除 key=2
// 实际: A变成B, B变成C, C被删除 (错误!)

4.3 正确的 key 用法

jsx
// ✅ 使用唯一稳定的 ID
{items.map(item => (
    <li key={item.id}>{item.name}</li>
))}

// ✅ 如果没有 ID,可以用内容生成
{items.map(item => (
    <li key={`${item.name}-${item.date}`}>{item.name}</li>
))}

5. JSX 进阶

5.1 条件渲染

jsx
// && 短路
{isLoggedIn && <UserMenu />}

// 三元表达式
{isLoggedIn ? <UserMenu /> : <LoginButton />}

// 提前返回
if (!data) return <Loading />;
return <Content data={data} />;

5.2 Fragment

jsx
// 不额外增加 DOM 节点
<>
    <Header />
    <Main />
    <Footer />
</>

// 带 key 的 Fragment
{items.map(item => (
    <Fragment key={item.id}>
        <dt>{item.term}</dt>
        <dd>{item.description}</dd>
    </Fragment>
))}

5.3 JSX 表达式

jsx
// JSX 中可以嵌入任何 JS 表达式
<div className={isActive ? 'active' : ''}>
    {user.name.toUpperCase()}
    {items.length > 0 && <List items={items} />}
</div>

// 但不能使用语句
// ❌ {if (condition) { ... }}
// ❌ {for (let i...) { ... }}

5.4 展开属性

jsx
const props = { id: 'btn', className: 'primary', onClick: handleClick };

// 展开所有属性
<button {...props}>Click</button>

// 选择性覆盖
<button {...props} className="secondary">Click</button>

6. $$typeof 安全机制

6.1 防止 XSS 攻击

javascript
// ReactElement 有特殊标识
const element = {
    $$typeof: Symbol.for('react.element'),
    type: 'div',
    props: { ... }
};

为什么用 Symbol?

javascript
// 攻击场景:用户输入存到数据库
const userInput = {
    type: 'div',
    props: {
        dangerouslySetInnerHTML: {
            __html: '<script>alert("hacked")</script>'
        }
    }
};

// 如果直接渲染 userInput 对象...
// 但因为没有 $$typeof: Symbol,React 不会当作 Element 处理
// Symbol 无法被 JSON 序列化,所以从服务端来的数据是安全的

7. 面试高频问题

Q1: 虚拟 DOM 一定比直接操作 DOM 快吗?

:不一定。虚拟 DOM 的优势不是"更快",而是:

  1. 足够快 + 声明式编程
  2. 在复杂更新场景下,自动做 Diff 通常比手动操作更可靠
  3. 简单场景下直接操作 DOM 可能更快

Q2: React 为什么不用模板语法?

  1. JSX 是 JS,拥有 JS 的全部能力
  2. 类型检查和 IDE 支持更好
  3. 没有新的语法需要学习(模板语言通常有自己的语法)
  4. 编译时优化潜力更大

Q3: key 为什么不能用 index?

:当列表发生重排、删除、插入时,index 会变化,导致:

  1. React 错误地复用节点
  2. 组件状态混乱
  3. 性能下降(不必要的重渲染)

Q4: Fragment 和 div 的区别?

  • Fragment 不创建真实 DOM 节点
  • 不影响 CSS 选择器和布局
  • 减少 DOM 层级

Q5: JSX 是如何被编译的?

  1. Babel 将 JSX 转换为 React.createElementjsx() 调用
  2. React 17+ 使用新的 jsx-runtime,无需手动引入 React
  3. 编译结果是创建 ReactElement 对象的函数调用

前端面试知识库