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: [] }); // 多个 children1.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 的优势不是"更快",而是:
- 足够快 + 声明式编程
- 在复杂更新场景下,自动做 Diff 通常比手动操作更可靠
- 简单场景下直接操作 DOM 可能更快
Q2: React 为什么不用模板语法?
答:
- JSX 是 JS,拥有 JS 的全部能力
- 类型检查和 IDE 支持更好
- 没有新的语法需要学习(模板语言通常有自己的语法)
- 编译时优化潜力更大
Q3: key 为什么不能用 index?
答:当列表发生重排、删除、插入时,index 会变化,导致:
- React 错误地复用节点
- 组件状态混乱
- 性能下降(不必要的重渲染)
Q4: Fragment 和 div 的区别?
答:
- Fragment 不创建真实 DOM 节点
- 不影响 CSS 选择器和布局
- 减少 DOM 层级
Q5: JSX 是如何被编译的?
答:
- Babel 将 JSX 转换为
React.createElement或jsx()调用 - React 17+ 使用新的 jsx-runtime,无需手动引入 React
- 编译结果是创建 ReactElement 对象的函数调用