React 原理详解

React 是当今最流行的前端框架之一,它的设计理念和实现原理值得深入学习。本文将从核心概念出发,详细解析 React 的工作原理,包括 Fiber 架构、渲染流程、diff 算法、key 的作用、批处理机制以及并发模式等关键内容,帮助你彻底理解 React 的内部工作机制。


一、Fiber 是什么

1.1 Fiber 的定义

Fiber 是 React 16 引入的一种新的核心数据结构,它是对 React 组件树的一种抽象表示,用于实现可中断的渲染过程优先级调度

通俗理解:

想象传统的 React 渲染是一条直线,一旦开始就不能停止,直到完成整个渲染过程。而 Fiber 则将渲染过程拆分成一个个可以中断的小任务,就像一条链条,每一节都是一个独立的工作单元。


1.2 Fiber 的核心概念

概念说明
Fiber 节点对应一个组件实例或 DOM 元素,包含类型、属性、状态等信息
Fiber 树由 Fiber 节点构成的树状结构,是 React 内部的虚拟 DOM 表示
Fiber 调度基于优先级的任务调度机制,可中断和恢复
Fiber 工作循环分为 render 阶段和 commit 阶段

1.3 Fiber 解决的问题

传统 React 16 之前的问题:

  • 渲染过程不可中断:一旦开始渲染,会一直占用主线程,导致用户交互卡顿
  • 调度不灵活:无法根据任务优先级调整执行顺序
  • 内存占用高:递归调用栈深度可能很高

Fiber 带来的改进:

  • 可中断的渲染:将渲染过程拆分成小任务,可随时中断
  • 优先级调度:根据任务优先级调整执行顺序
  • 更好的错误处理:局部错误不会影响整个应用
  • 支持并发模式:为 React 18 的并发特性奠定基础

1.4 Fiber 节点结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Fiber 节点的核心结构
const fiber = {
// 类型信息
tag: 0, // 节点类型(函数组件、类组件、原生 DOM 等)
type: null, // 组件类型
key: null, // 唯一标识

// 结构信息
return: null, // 父节点
child: null, // 第一个子节点
sibling: null, // 兄弟节点

// 状态信息
stateNode: null, // 真实 DOM 节点或组件实例
memoizedProps: null, // 上次渲染的 props
memoizedState: null, // 上次渲染的 state

// 调度信息
flags: 0, // 副作用标记
subtreeFlags: 0, // 子树副作用标记
updateQueue: null, // 更新队列
alternate: null, // 双缓冲机制中的另一个 Fiber

// 优先级信息
priority: 0, // 任务优先级
lane: 0, // 优先级车道

// 其他信息
effectTag: 0, // 副作用类型
nextEffect: null, // 下一个有副作用的节点
firstEffect: null, // 第一个有副作用的节点
lastEffect: null, // 最后一个有副作用的节点
};

1.5 Fiber 树的构建过程

  1. 初始化:创建根 Fiber 节点
  2. 遍历:深度优先遍历组件树,为每个组件创建对应的 Fiber 节点
  3. 标记:标记需要更新的节点(添加、删除、更新)
  4. 提交:将标记的变更应用到真实 DOM

二、渲染流程

2.1 传统渲染流程 vs Fiber 渲染流程

传统渲染流程(React 15):

1
2
3
┌─────────────┐     ┌─────────────┐
│ 递归渲染 │ ──▶ │ 批量更新 │
└─────────────┘ └─────────────┘

Fiber 渲染流程(React 16+):

1
2
3
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ 调度阶段 │ ──▶ │ 渲染阶段 │ ──▶ │ 提交阶段 │
└─────────────┘ └─────────────┘ └─────────────┘

2.2 渲染流程详解

阶段一:调度阶段(Scheduler)

核心任务:

  • 接收更新请求(setState、useState 等)
  • 计算任务优先级
  • 安排任务执行顺序

关键步骤:

  1. 任务入队:将更新任务加入调度队列
  2. 优先级计算:根据任务类型和时间计算优先级
  3. 任务调度:选择最高优先级的任务执行

阶段二:渲染阶段(Render)

核心任务:

  • 构建 Fiber 树
  • 标记需要更新的节点
  • 收集副作用

关键步骤:

  1. 创建 WorkInProgress 树:基于 Current 树创建
  2. 深度优先遍历
    • 计算新的状态和 props
    • 比较新旧节点,标记变更
    • 收集副作用(effects)
  3. 完成渲染:生成 Effect 链表

注意: 渲染阶段是可中断的,如果有更高优先级的任务,会暂停当前渲染。


阶段三:提交阶段(Commit)

核心任务:

  • 将渲染阶段的变更应用到真实 DOM
  • 执行副作用(如 useEffect)
  • 更新 ref

关键步骤:

  1. 准备提交:获取 Effect 链表
  2. DOM 变更
    • 执行 DOM 插入、更新、删除
    • 更新 ref
  3. 副作用执行
    • 执行 useEffect cleanup
    • 执行 useEffect 回调
  4. 完成提交:将 WorkInProgress 树设置为 Current 树

注意: 提交阶段是不可中断的,必须一次性完成。


2.3 渲染流程示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────┐
│ React 渲染流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 调度阶段 │ ──▶ │ 渲染阶段 │ ──▶ │ 提交阶段 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 1. 接收更新请求 │
│ 2. 计算任务优先级 │
│ 3. 安排任务执行顺序 │
│ │
│ 4. 构建 WorkInProgress 树 │
│ 5. 深度优先遍历 │
│ 6. 标记变更,收集副作用 │
│ 7. 生成 Effect 链表 │
│ │
│ 8. 应用 DOM 变更 │
│ 9. 执行副作用 │
│ 10. 更新 ref │
│ 11. 完成提交 │
│ │
└─────────────────────────────────────────────────────────────────┘

2.4 生命周期与渲染流程的关系

旧生命周期(React 16.3 之前):

  • componentWillMount → 渲染阶段开始前
  • componentWillReceiveProps → 渲染阶段
  • shouldComponentUpdate → 渲染阶段(决定是否继续)
  • componentWillUpdate → 渲染阶段
  • render → 渲染阶段
  • componentDidMount → 提交阶段后
  • componentDidUpdate → 提交阶段后
  • componentWillUnmount → 提交阶段(删除时)

新生命周期(React 16.3+):

  • getDerivedStateFromProps → 渲染阶段开始
  • shouldComponentUpdate → 渲染阶段(决定是否继续)
  • render → 渲染阶段
  • getSnapshotBeforeUpdate → 提交阶段开始前
  • componentDidMount → 提交阶段后
  • componentDidUpdate → 提交阶段后
  • componentWillUnmount → 提交阶段(删除时)
  • componentDidCatch → 错误边界

三、diff 算法策略

3.1 diff 算法的核心思想

diff 算法是 React 用来比较新旧虚拟 DOM 树,找出需要更新的节点的算法。

核心原则:

  • 只比较同级节点:不跨层级比较
  • key 是标识:使用 key 来识别节点
  • 类型不同则替换:节点类型不同,直接替换整个子树
  • 类型相同则更新:节点类型相同,更新属性和子节点

3.2 diff 算法的优化策略

策略一:分层比较

1
2
3
4
5
6
7
8
9
10
┌─────────────┐     ┌─────────────┐
│ 父节点 A │ │ 父节点 A │
├─────────────┤ ├─────────────┤
│ ┌──────┐ │ │ ┌──────┐ │
│ │ B │ │ │ │ B │ │ ← 比较同级
│ └──────┘ │ │ └──────┘ │
│ ┌──────┐ │ │ ┌──────┐ │
│ │ C │ │ │ │ D │ │ ← 比较同级(类型不同,替换)
│ └──────┘ │ │ └──────┘ │
└─────────────┘ └─────────────┘

优势: 时间复杂度从 O(n³) 降低到 O(n)


策略二:key 识别

1
2
3
4
5
旧节点:[A(key:1), B(key:2), C(key:3)]
新节点:[C(key:3), A(key:1), B(key:2)]

// 有 key:只需移动节点位置
// 无 key:会删除并重建所有节点

优势: 避免不必要的 DOM 操作,提高性能


策略三:类型判断

  • 类型相同:更新属性,递归比较子节点
  • 类型不同:直接替换整个子树
1
2
3
4
5
6
7
8
9
if (oldNode.type !== newNode.type) {
// 类型不同,直接替换
replaceOldNodeWithNewNode();
} else {
// 类型相同,更新属性
updateNodeProperties(oldNode, newNode);
// 递归比较子节点
reconcileChildren(oldNode, newNode);
}

3.3 diff 算法的具体实现

单节点 diff

处理逻辑:

  1. 比较 key 是否相同
  2. 比较 type 是否相同
  3. 根据比较结果执行相应操作

情况分析:
| 情况 | key | type | 操作 |
|——|—–|——|——|
| 1 | 相同 | 相同 | 更新属性 |
| 2 | 相同 | 不同 | 替换节点 |
| 3 | 不同 | - | 创建新节点 |


多节点 diff

处理逻辑:

  1. 第一轮:从头部开始比较,相同则继续,不同则停止
  2. 第二轮:从尾部开始比较,相同则继续,不同则停止
  3. 第三轮:处理剩余节点
    • 新节点多:创建新节点
    • 旧节点多:删除多余节点
    • 节点移动:通过 key 查找位置并移动

示例:

1
2
3
4
5
6
旧节点:A B C D E
新节点:A B F C D

// 第一轮:A B 相同
// 第二轮:D 相同
// 第三轮:处理 F C(F 是新节点,C 移动位置)

3.4 diff 算法的时间复杂度

理想情况: O(n) —— 只需要遍历一次节点
最坏情况: O(n²) —— 所有节点都需要移动位置

实际情况: 由于使用了 key 和分层比较,通常接近 O(n)


四、key 的作用原理

4.1 key 的定义

key 是 React 用来识别组件的唯一标识符,在列表渲染时尤为重要。

key 的要求:

  • 唯一性:在兄弟节点中必须唯一
  • 稳定性:不随渲染而变化
  • 可预测性:最好是稳定的 ID,而不是索引

4.2 key 的作用

作用一:识别节点

有 key 时:

  • React 可以通过 key 快速识别哪些节点是相同的
  • 避免不必要的 DOM 操作
  • 保持组件状态

无 key 时:

  • React 会使用索引作为默认 key
  • 可能导致组件状态混乱
  • 性能下降

作用二:优化渲染性能

例子:

1
2
3
4
5
6
7
8
9
// 不好的做法(使用索引)
const list = items.map((item, index) => (
<Item key={index} data={item} />
));

// 好的做法(使用唯一 ID)
const list = items.map(item => (
<Item key={item.id} data={item} />
));

性能差异:

  • 当列表顺序变化时,使用索引会导致所有节点重新渲染
  • 使用唯一 ID 只需要移动节点位置

4.3 key 的工作原理

在 diff 过程中的作用:

  1. 创建 key-index 映射:React 会为旧节点创建 key 到索引的映射
  2. 查找匹配节点:通过 key 快速查找新节点在旧树中的位置
  3. 确定操作类型:根据查找结果确定是更新、移动还是创建

源码层面:

1
2
3
4
5
6
7
8
9
// 简化的 key 查找逻辑
function findIndexByKey(keys, key) {
for (let i = 0; i < keys.length; i++) {
if (keys[i] === key) {
return i;
}
}
return -1;
}

4.4 key 的最佳实践

推荐做法:

  • 使用后端返回的唯一 ID
  • 确保 key 在兄弟节点中唯一
  • 不要使用随机值作为 key
  • 不要在不同列表中使用相同的 key

避免做法:

  • 使用索引作为 key(特别是列表会排序或筛选时)
  • 使用 Math.random() 作为 key
  • 在不同组件间复用相同的 key

五、React 18 批处理

5.1 批处理的定义

批处理(Batching) 是 React 将多个状态更新合并为一次渲染的机制,以提高性能。

通俗理解:

想象你去超市购物,一次买完所有东西再结账,比买一件结一次账要高效得多。React 的批处理就是类似的道理。


5.2 批处理的演进

React 17 及之前:

  • 仅在事件处理器内批处理:异步代码(setTimeout、Promise 等)中的更新不会被批处理

React 18:

  • 自动批处理:无论在何处触发的更新都会被批处理
  • 更广泛的批处理:包括 setTimeout、Promise、原生事件等

5.3 批处理的工作原理

核心机制:

  1. 收集更新:将多个 setState 调用收集到队列中
  2. 延迟执行:不立即执行更新,而是等待当前事件循环结束
  3. 合并更新:将多个更新合并为一次渲染
  4. 执行渲染:只进行一次 DOM 更新和渲染

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// React 17:只有事件处理器内的更新会被批处理
function handleClick() {
// 会被批处理:只触发一次渲染
setCount(c => c + 1);
setFlag(f => !f);
}

// 不会被批处理:触发两次渲染
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);

// React 18:所有更新都会被批处理
// 无论在哪里,都会只触发一次渲染
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);

Promise.resolve().then(() => {
setCount(c => c + 1);
setFlag(f => !f);
});

5.4 批处理的优势

优势说明
减少渲染次数合并多次更新为一次,减少 DOM 操作
提高性能减少浏览器重绘和回流
保持 UI 一致性避免中间状态的闪烁
简化代码不需要手动管理更新时机

5.5 如何退出批处理

使用 flushSync:

1
2
3
4
5
6
7
8
9
import { flushSync } from 'react-dom';

function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// 这里的更新会在上面的更新之后立即执行
setFlag(f => !f);
}

适用场景:

  • 需要立即看到更新结果的场景
  • 与非 React 库集成时
  • 特定的性能优化场景

六、并发模式基本概念

6.1 并发模式的定义

并发模式(Concurrent Mode) 是 React 18 引入的一种新的渲染模式,它允许 React 同时处理多个任务,并根据优先级调整执行顺序。

通俗理解:

传统模式下,React 一次只做一件事,直到完成。并发模式下,React 可以同时做多个事情,并且可以根据重要性调整顺序。


6.2 并发模式的核心概念

概念说明
优先级任务的重要程度,决定执行顺序
时间切片将渲染任务分成小片段,避免阻塞主线程
可中断渲染渲染过程可以被更高优先级的任务中断
Suspense允许组件在数据加载完成前显示 fallback

6.3 并发模式的优势

优势说明
更流畅的用户体验高优先级任务(如用户输入)可以优先执行
减少卡顿渲染过程可中断,不会阻塞主线程
更好的资源利用可以在空闲时间处理低优先级任务
支持 Suspense简化数据加载的处理

6.4 并发模式的工作原理

核心机制:

  1. 任务调度:根据优先级调度任务
  2. 时间切片:将渲染拆分成 16ms 以内的小任务
  3. 可中断渲染:允许高优先级任务中断低优先级任务
  4. 自动批处理:合并更新以提高性能

执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────────────┐
│ 并发模式执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 接收更新请求 │
│ 2. 计算任务优先级 │
│ 3. 调度任务执行 │
│ 4. 执行时间切片(≤16ms) │
│ 5. 检查是否有高优先级任务 │
│ ├── 有 → 暂停当前任务,执行高优先级任务 │
│ └── 无 → 继续执行当前任务 │
│ 6. 完成渲染,提交变更 │
│ │
└─────────────────────────────────────────────────────────────────┘

6.5 并发模式的 API

开启并发模式:

1
2
3
4
5
6
// React 18 之前
ReactDOM.render(<App />, document.getElementById('root'));

// React 18
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Suspense:

1
2
3
<Suspense fallback={<Loading />}>
<Component />
</Suspense>

useTransition:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useTransition } from 'react';

function App() {
const [isPending, startTransition] = useTransition();

function handleClick() {
startTransition(() => {
// 低优先级更新
setCount(c => c + 1);
});
}

return (
<div>
{isPending && <p>Loading...</p>}
<button onClick={handleClick}>Increment</button>
</div>
);
}

useDeferredValue:

1
2
3
4
5
6
7
8
9
10
11
12
import { useDeferredValue } from 'react';

function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);

return (
<div>
{/* 使用延迟值,避免输入时的卡顿 */}
{deferredQuery && <Results query={deferredQuery} />}
</div>
);
}

6.6 并发模式的应用场景

场景优势
用户输入输入时不会卡顿,响应更及时
数据加载可以显示加载状态,避免空白页面
大型列表滚动时更流畅,不会阻塞主线程
动画效果动画更平滑,不受渲染影响

七、React 原理总结

7.1 核心概念关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────────┐
│ React 核心概念 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Fiber │ ──▶ │ 渲染流程 │ │
│ └─────────────┘ └─────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 并发模式 │ ◀── │ 批处理 │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ diff 算法 │ │
│ └─────────────┘ │
│ ▲ │
│ │ │
│ ┌─────────────┐ │
│ │ key │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

7.2 关键原理总结

概念核心要点
Fiber可中断的渲染单元,支持优先级调度
渲染流程调度 → 渲染 → 提交,渲染阶段可中断
diff 算法分层比较、key 识别、类型判断
key唯一标识符,优化渲染性能,保持组件状态
批处理合并更新,减少渲染次数,提高性能
并发模式优先级调度,时间切片,可中断渲染

7.3 学习建议

  1. 理解核心概念:Fiber、渲染流程、diff 算法是基础
  2. 实践验证:通过调试工具观察渲染过程
  3. 对比学习:与其他框架(如 Vue)的渲染机制对比
  4. 关注版本变化:React 18 的并发特性是重点
  5. 阅读源码:深入理解 React 的实现细节

7.4 常见面试题

Q:Fiber 是什么?它解决了什么问题?

Fiber 是 React 16 引入的核心数据结构,它将渲染过程拆分成可中断的小任务,支持优先级调度,解决了传统 React 渲染过程不可中断导致的卡顿问题。

Q:React 的渲染流程是怎样的?

渲染流程分为三个阶段:调度阶段(接收更新,计算优先级)、渲染阶段(构建 Fiber 树,标记变更)、提交阶段(应用 DOM 变更,执行副作用)。渲染阶段是可中断的,提交阶段不可中断。

Q:diff 算法的优化策略有哪些?

主要优化策略包括:分层比较(只比较同级节点)、key 识别(使用 key 快速定位节点)、类型判断(类型不同直接替换)。

Q:key 的作用是什么?为什么不能使用索引作为 key?

key 是 React 用来识别组件的唯一标识符,它能帮助 React 快速定位需要更新的节点,避免不必要的 DOM 操作。使用索引作为 key 在列表排序或筛选时会导致组件状态混乱,因为索引会随位置变化而变化。

Q:React 18 的批处理有什么改进?

React 18 引入了自动批处理,无论在何处触发的更新(包括 setTimeout、Promise 等)都会被批处理,而 React 17 及之前只在事件处理器内批处理。

Q:并发模式的核心特性是什么?

并发模式的核心特性包括:优先级调度、时间切片、可中断渲染、Suspense 支持。它允许 React 同时处理多个任务,并根据优先级调整执行顺序,提供更流畅的用户体验。


八、总结

React 的设计理念和实现原理是前端开发中的重要知识,掌握这些原理不仅能帮助你写出更高效的 React 代码,还能让你在面试中脱颖而出。

核心要点:

  • Fiber 是 React 16+ 的核心数据结构,支持可中断渲染
  • 渲染流程 分为调度、渲染、提交三个阶段
  • diff 算法 通过分层比较、key 识别等策略优化性能
  • key 是优化渲染的关键,应使用唯一稳定的标识符
  • React 18 引入了自动批处理和并发模式
  • 并发模式 提供了更好的用户体验和性能

希望本文能帮助你彻底理解 React 的工作原理,在前端开发中更加游刃有余!