核心概念
本章深入 React 的核心机制:JSX 编译产物、Virtual DOM、Fiber 调和(Reconciliation),以及渲染与提交分离的流程。
JSX 的本质
JSX 是 React.createElement() 的语法糖。Babel 或 Vite 的 React 插件在构建时做转换:
// 手写的 JSX
<div className="box">
<h1>Hello</h1>
</div>
// 编译后 ≈
import { jsx as _jsx } from 'react/jsx-runtime';
_jsx('div', { className: 'box', children: _jsx('h1', children: 'Hello') });🔬 深入原理:React 17+ 引入了新的 JSX 转换(
react/jsx-runtime),不再需要手动import React from 'react'。编译器自动注入jsx/jsxs函数。
JSX 的编译产物是一个普通的 JavaScript 对象(React Element):
// 一个 React Element 的形状
{
$$typeof: Symbol(react.element),
type: 'div', // 原生标签 → 字符串;自定义组件 → 函数引用
key: null,
ref: null,
props: {
className: 'box',
children: {
$$typeof: Symbol(react.element),
type: 'h1',
props: { children: 'Hello' }
}
}
}React Element vs Component vs DOM
| 概念 | 是什么 | 由谁创建 | 何时生成 |
|---|---|---|---|
| React Element | 轻量 JS 对象,描述 UI | createElement / JSX |
render 时 |
| Component | 函数(或 class),接收 props 返回 Element | 开发者 | 定义时 |
| Fiber | React 内部的工作单元节点 | React | 调和阶段 |
| DOM Node | 浏览器真实节点 | 浏览器 | 提交阶段 |
关键区别:Component 是蓝图,Element 是图纸,DOM 是建筑。
Virtual DOM 与 Reconciliation
什么是 Virtual DOM
Virtual DOM 是一个轻量的 JavaScript 对象树,它是真实 DOM 的抽象表示。React 通过对比新旧两棵 Virtual DOM 树(Diffing),计算出最小的 DOM 操作。
调和流程(Reconciliation)
State 变化
│
▼
标记需要更新的 Fiber 节点
│
▼
【Render 阶段】构建新的 Fiber 树,与当前树做 Diff
│
▼
【Commit 阶段】将差异批量应用到真实 DOM
│
▼
浏览器重绘两个阶段分离的意义:Render 阶段可以被中断、恢复、丢弃(异步可中断),Commit 阶段必须同步完成。
🔬 深入原理:Render 阶段走的是 Fiber 树的"替代"分支(workInProgress),所有计算都在内存中完成。Commit 阶段才一次性修改 DOM,用户不会看到中间状态。
Fiber 架构
Fiber(纤维)是 React 16+ 的调和引擎核心。每个组件实例对应一个 Fiber Node。
Fiber Node 的关键字段
{
tag: FunctionComponent, // 节点类型
stateNode: 组件实例,
child: 第一个子节点,
sibling: 下一个兄弟节点,
return: 父节点,
alternate: 对应的 workInProgress / current,
memoizedState: 当前 hooks 链表,
pendingProps: 即将处理的 props,
memoizedProps: 上次渲染的 props,
effectTag: 副作用标记(Placement/Update/Deletion)
}Fiber 树的双缓冲
React 维护两颗 Fiber 树:
- current:对应当前屏幕上的 UI
- workInProgress(WIP):正在内存中构建的新树
Diff 完成后,WIP 树切换为新的 current(指针交换,O(1))。这就是 React 的"双缓冲"机制。
工作循环(Work Loop)
┌───────────────────────────────┐
│ beginWork() — 递阶段 │
│ 遍历 Fiber 树(DFS) │
│ 对比新旧 props,标记副作用 │
│ 可被 shouldYield() 中断 │
└──────────────┬────────────────┘
│ (子节点完成)
▼
┌───────────────────────────────┐
│ completeWork() — 归阶段 │
│ 收集副作用链 │
│ 创建/更新 DOM 节点 │
└──────────────┬────────────────┘
│ (归到根节点)
▼
┌───────────────────────────────┐
│ Commit Phase — 提交阶段 │
│ BeforeMutation → Mutation │
│ → Layout (useLayoutEffect) │
└───────────────────────────────┘Diffing 算法(三点策略)
React 的 Diff 基于三点启发式假设(O(n) 复杂度):
1. 类型不同 → 整棵树重建
// old
<div><span>Hello</span></div>
// new
<header><span>Hello</span></header>
// 结果:div 及其子节点全部销毁,header 及其子节点全部新建
// 即使 span 内容相同也不会保留2. key 标识列表节点
// 不用 key(索引当作 key):低效
{todos.map((todo, i) => <Todo key={i} {...todo} />)}
// 用稳定 id:正确
{todos.map(todo => <Todo key={todo.id} {...todo} />)}🚨 陷阱:用数组索引做 key 在列表顺序变化时会导致组件状态错位、不必要的 DOM 更新甚至 input 焦点错乱。
3. 同一子树内只同层比较
React 不会跨层级比较节点。如果节点从一层移到另一层,React 会销毁后再重建,而非移动。
State 的批处理(Batching)
React 18+ 会对所有状态更新自动批处理:
function handleClick() {
setCount(c => c + 1); // 不会立即触发渲染
setFlag(f => !f); // 不会立即触发渲染
// React 将两次更新合并为一次渲染
}
// 异步回调中也会批处理(React 18+)
async function handleAsync() {
const data = await fetch('/api');
setData(data); // 与下面的 setLoading 合并
setLoading(false); // 只触发一次渲染
}🔬 深入原理:React 18 前,只在事件处理器内批处理。异步回调(setTimeout、Promise)中的多次 setState 各触发一次渲染。React 18 引入
createRoot后全部自动批处理。
需要跳过批处理(罕见)
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // 立即提交到 DOM
});
// 此时 DOM 已更新渲染 ≠ 提交到 DOM
这个区分是理解 React 性能的核心:
| 阶段 | 执行内容 | 可中断 | 副作用 |
|---|---|---|---|
| Render | 调用组件函数,计算差异 | ✅ 可中断 | 不应包含副作用 |
| Commit | 修改 DOM、触发 ref 回调 | ❌ 同步 | 副作用执行时机 |
function Component() {
console.log('Render 阶段 — 可能被多次调用(StrictMode 下确如此)');
useEffect(() => {
console.log('Commit 阶段 — 只在 DOM 提交后执行一次');
});
}💡 最佳实践:不要在组件函数体中做副作用(修改全局变量、发请求、操作 DOM)。所有副作用都放入
useEffect或事件处理器。
核心概念速查表
| 概念 | 一句话解释 |
|---|---|
| JSX | 返回 React Element 的 JavaScript 扩展语法 |
| Virtual DOM | 真实 DOM 的 JS 对象抽象层 |
| Fiber | 每个组件实例对应的内部工作单元 |
| Reconciliation | React 对比新旧树并决定最小 DOM 操作的流程 |
| Render Phase | 组件函数执行 + Diffing,可中断 |
| Commit Phase | DOM 更新 + 副作用执行,不可中断 |
| Batching | 合并多个 setState 为一次渲染 |
| Key | 列表 Diff 优化的标识,必须稳定唯一 |