React 面试完全指南
按 React 执行顺序深度解析 | 从架构到实现
模块一:React 渲染流程总览
📍 执行位置:整个 React 应用的起点,理解这个流程是理解后续所有模块的基础
Q1: React 渲染的三个阶段是什么?
React 的渲染流程分为三个核心阶段,每个阶段由不同的模块负责:
┌─────────────────────────────────────────────────────────────────┐
│ React 渲染流程总览 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Scheduler │───▶│ Reconciler │───▶│ Renderer │ │
│ │ 调度器 │ │ 协调器 │ │ 渲染器 │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 决定何时执行 计算如何更新 把结果写入DOM │
│ (优先级调度) (Diff算法) (DOM操作) │
│ │
└─────────────────────────────────────────────────────────────────┘
1. Scheduler(调度器) 负责决定何时开始渲染。它使用优先级队列管理更新任务,高优先级任务(如用户输入)可以打断低优先级任务(如数据获取后的渲染)。
2. Reconciler(协调器) 负责计算哪些 DOM 需要改变。它遍历 Fiber 树,通过 Diff 算法比较新旧 Fiber,标记需要更新的节点。这个过程是可中断的。
3. Renderer(渲染器) 负责将 Reconciler 的计算结果写入宿主环境。不同的平台有不同的渲染器:ReactDOM(浏览器)、ReactNative(移动端)、ReactArt(Canvas)等。
Q2: Render 阶段和 Commit 阶段有什么区别?
| Render 阶段(可中断) | Commit 阶段(不可中断) |
|---|---|
| 执行组件函数,生成新的 Fiber 树 | 将变更写入 DOM |
| 运行 Diff 算法 | 执行 useLayoutEffect |
| 计算需要更新的部分 | 调度 useEffect |
| 可以被打断(高优先级任务插入) | 必须一次性完成 |
| 纯计算,无副作用 | 有副作用,可见变化 |
┌──────────────────────────────────────────────────────────────────┐
│ Render vs Commit 阶段 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Render 阶段 Commit 阶段 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 开始渲染 │ │ 提交变更 │ │
│ │ ↓ │ │ ↓ │ │
│ │ 执行组件函数 │ │ beforeMutation │ │
│ │ ↓ │ │ ↓ │ │
│ │ Diff 比较 │ ◀──可中断──▶ │ mutation │ │
│ │ ↓ │ │ ↓ │ │
│ │ 生成 EffectList│ │ layout │ │
│ │ ↓ │ │ ↓ │ │
│ │ 等待提交 │ │ 完成 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 时间切片可拆分 必须原子完成 │
│ │
└──────────────────────────────────────────────────────────────────┘
Q3: 为什么 React 需要这样的架构设计?
核心问题:JavaScript 是单线程的
在 React 15 及之前,React 采用 Stack 架构,递归遍历虚拟 DOM 树。这个过程是同步的,一旦开始就必须完成。如果组件树很大,就会长时间阻塞主线程,导致动画卡顿、输入无响应。
┌─────────────────────────────────────────────────────────────────┐
│ Stack 架构的问题 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 主线程时间线 │
│ ┌────────────────────────────────────────────┬────────┐ │
│ │ React 渲染(阻塞中...) │ 用户 │ │
│ │ 无法响应输入 │ 输入 │ │
│ └────────────────────────────────────────────┴────────┘ │
│ │
│ 问题:渲染期间用户输入无响应,体验差 │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Fiber 架构的解决方案 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 主线程时间线 │
│ ┌──────┬────┬──────┬────┬──────┬────┬──────┬────┐ │
│ │渲染1 │输入│渲染2 │动画│渲染3 │输入│渲染4 │... │ │
│ └──────┴────┴──────┴────┴──────┴────┴──────┴────┘ │
│ │
│ 优势:渲染任务被拆分成小单元,可以穿插其他任务 │
│ │
└─────────────────────────────────────────────────────────────────┘
Fiber 架构的优势:
- 可中断渲染:大任务拆分成小单元,每个单元执行后检查是否有更高优先级任务
- 优先级调度:用户输入 > 动画 > 数据更新
- 渐进式渲染:先渲染可见区域,后渲染屏幕外内容
模块二:Fiber 架构基础
📍 执行位置:Scheduler 调度后,Reconciler 开始工作,Fiber 是 Reconciler 的核心数据结构
Q1: 什么是 Fiber?为什么需要它?
Fiber 有三层含义:
- 架构层面:React 16 采用的新架构,支持可中断渲染
- 数据结构层面:每个 Fiber 节点对应一个组件或 DOM 元素
- 工作单元层面:每个 Fiber 是一个可执行的工作单元
┌─────────────────────────────────────────────────────────────────┐
│ Fiber 节点结构示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Fiber 节点 │
│ ┌──────────────┐ │
│ │ type │──▶ div / App / "text" │
│ │ key │──▶ 用于 Diff 标识 │
│ │ props │──▶ 属性对象 │
│ │ stateNode │──▶ 真实 DOM / 组件实例 │
│ │ │ │
│ │ memoizedState│──▶ Hooks 链表头 │
│ │ updateQueue │──▶ 更新队列 │
│ │ flags │──▶ 副作用标记 │
│ └──────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ return child sibling │
│ (父节点) (第一个子) (下一个兄弟) │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: Fiber 节点的核心字段有哪些?
const fiber = {
// 静态数据(描述组件)
type: 'div', // 元素类型:div、函数组件、类组件
key: 'list-item-1', // 用于 Diff 算法识别节点身份
props: { className: 'item' }, // 属性对象
stateNode: domNode, // 对应的真实 DOM 或组件实例
// 树结构(连接其他 Fiber)
return: parentFiber, // 父节点(为什么叫 return?因为完成后 return 给父节点)
child: firstChildFiber, // 第一个子节点
sibling: nextFiber, // 下一个兄弟节点
// 状态相关(关联 Hooks)
memoizedState: hook, // Hooks 链表的头节点!
updateQueue: queue, // 待处理的更新队列
// 副作用标记
flags: Placement, // 标记需要执行的操作:插入、更新、删除
lanes: priority, // 优先级(React 18 的 Lane 模型)
// 双缓冲
alternate: currentFiber // 指向另一棵树的对应节点
}
关键理解: useState 的状态就存储在
fiber.memoizedState指向的链表中!多个 Hook 通过链表连接,顺序决定了它们的对应关系。
Q3: 什么是双缓冲机制?
双缓冲是 React 用于优化渲染的技术。React 同时维护两棵 Fiber 树:
┌─────────────────────────────────────────────────────────────────┐
│ 双缓冲机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Current Tree WorkInProgress Tree │
│ (当前屏幕显示) (正在构建中) │
│ │
│ App App │
│ / \ / \ │
│ Div Span ◀──alternate──▶ Div Span │
│ | | │
│ Text Text │
│ │
│ fiberRootNode.current ───────────▶ current tree root │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Render 阶段:构建 WorkInProgress Tree │ │
│ │ Commit 阶段:一次性切换 current 指针 │ │
│ │ │ │
│ │ fiberRootNode.current = workInProgress │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
为什么需要双缓冲?
- 如果直接修改 Current Tree,用户可能看到不完整的 UI
- 在 WorkInProgress Tree 上构建完成后,一次性切换,保证 UI 一致性
- 两棵树通过
alternate字段互相引用,复用内存
Q4: Fiber 和 Hooks 是什么关系?
**核心关系:**每个 Fiber 节点的 memoizedState 字段存储该组件所有 Hooks 的状态链表。
┌─────────────────────────────────────────────────────────────────┐
│ Fiber 与 Hooks 的关系 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ function Counter() { │
│ const [count, setCount] = useState(0); // Hook 1 │
│ const [name, setName] = useState('Tom'); // Hook 2 │
│ useEffect(() => {}, []); // Hook 3 │
│ } │
│ │
│ 对应的 Fiber.memoizedState 链表: │
│ │
│ fiber.memoizedState │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Hook 1 │───▶│ Hook 2 │───▶│ Hook 3 │ │
│ │ useState │ │ useState │ │ useEffect │ │
│ │ │ │ │ │ │ │
│ │ memoizedState│ │ memoizedState│ │ memoizedState│ │
│ │ = 0 │ │ = 'Tom' │ │ = effect │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Hook 的顺序决定了它在链表中的位置! │
│ 所以不能在条件语句中使用 Hooks │
│ │
└─────────────────────────────────────────────────────────────────┘
为什么 Hooks 不能在条件语句中使用? 因为 Hooks 通过链表顺序来匹配状态。如果第一次渲染执行了 3 个 Hook,第二次渲染因为条件只执行了 2 个,Hook 和状态的对应关系就会错乱。
模块三:Diff 算法原理(重点)
📍 执行位置:Reconciler 阶段的核心:比较新旧 Fiber 树,计算最小更新
Q1: 为什么需要 Diff 算法?
从 JSX 到 DOM 的过程:
┌─────────────────────────────────────────────────────────────────┐
│ JSX → DOM 的转换过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ JSX 代码 │
│ ┌─────────────────────────────────────────┐ │
│ │ <div className="app"> │ │
│ │ <Header /> │ │
│ │ <List items={data} /> │ │
│ │ </div> │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ Babel 编译 │
│ ┌─────────────────────────────────────────┐ │
│ │ React.createElement('div', ...) │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ 生成 │
│ ┌─────────────────────────────────────────┐ │
│ │ React Element (虚拟 DOM) │ │
│ │ { type: 'div', props: {...} } │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ Diff 算法比较 │
│ ┌─────────────────────────────────────────┐ │
│ │ 新旧 Fiber 树对比 │ │
│ │ 标记需要更新的节点 │ │
│ └─────────────────────────────────────────┘ │
│ │ │
│ ▼ Renderer │
│ ┌─────────────────────────────────────────┐ │
│ │ 真实 DOM │ │
│ │ <div class="app">...</div> │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
**Diff 的目标:**找到从旧树到新树的最小操作序列。
**问题:**比较两棵树的差异是 O(n³) 复杂度,太慢了!
**React 的策略:**通过三个假设,将复杂度降到 O(n)。
Q2: Diff 算法的三个核心策略是什么?
策略一:只比较同一层级,不跨层级比较
React 假设 DOM 节点不会跨层级移动。如果父节点变了,整个子树都会重新创建,不会尝试复用子节点。
┌───────────────────────────────────────────────────┐
│ 旧树 新树 │
│ │
│ A A' │
│ /|\ /|\ │
│ B C D B C D │
│ │
│ 只比较同一层级的节点:A vs A', B vs B, C vs C... │
│ 不会比较 B 和 C 的位置关系 │
└───────────────────────────────────────────────────┘
策略二:类型不同,直接替换整个子树
如果节点类型不同(如 div 变成 span,或 App 变成 List),React 会直接销毁旧节点及其子树,创建新节点。
┌───────────────────────────────────────────────────┐
│ 旧树 新树 │
│ │
│ div span ← 类型不同 │
│ | | │
│ Text Text │
│ │
│ 结果:销毁整个 div 子树,创建新的 span 子树 │
│ 不会尝试复用 Text 节点 │
└───────────────────────────────────────────────────┘
策略三:用 key 标识节点身份
对于同一层级的多个子节点,React 用 key 来判断"这个节点移动了"还是"这是个新节点"。
┌───────────────────────────────────────────────────┐
│ 旧列表 新列表 │
│ │
│ <li key="a">A</li> <li key="b">B</li> │
│ <li key="b">B</li> <li key="a">A</li> │
│ <li key="c">C</li> <li key="c">C</li> │
│ │
│ 有 key:React 知道 A 和 B 只是交换了位置 │
│ 无 key:React 会认为 A 变成了 B,B 变成了 A │
│ 导致全部重新渲染 │
└───────────────────────────────────────────────────┘
Q3: 单节点 Diff 的过程是怎样的?
**场景:**父组件渲染一个子节点,需要判断能否复用旧的 Fiber。
┌─────────────────────────────────────────────────────────────────┐
│ 单节点 Diff 流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 新的 React Element 旧的 Fiber │
│ { type: 'div', key: 'a' } { type: 'div', key: 'a' } │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Step 1: 比较 key │ │
│ │ ┌─────────────┐ │ │
│ │ │ key 相同? │──否──▶ 创建新 Fiber,标记删除旧 Fiber │ │
│ │ └─────────────┘ │ │
│ │ │ 是 │ │
│ │ ▼ │ │
│ │ Step 2: 比较 type │ │
│ │ ┌──────────────┐ │ │
│ │ │ type 相同? │──否──▶ 创建新 Fiber,标记删除旧 Fiber │ │
│ │ └──────────────┘ │ │
│ │ │ 是 │ │
│ │ ▼ │ │
│ │ Step 3: 复用旧 Fiber │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ 复用 DOM 节点 │ │ │
│ │ │ 更新 props │ │ │
│ │ │ 标记 Update flag │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
类比理解:
想象你在整理衣柜。拿出一件衣服(新 Element),看看柜子里有没有同一件(比较 key 和 type):
- 如果找到了同一件,就放回去,可能整理一下(更新 props)
- 如果找到的是不同的衣服,就把旧的扔掉,放新的进去
Q4: 多节点 Diff 的四阶段是怎样的?
**场景:**一个父节点有多个子节点,需要比较新旧列表。
┌─────────────────────────────────────────────────────────────────┐
│ 多节点 Diff 四阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧子节点: [A, B, C, D] │
│ 新子节点: [A, B, E, F] │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 第一阶段:从左往右同步遍历 │ │
│ │ │ │
│ │ A vs A → key 相同,type 相同 → 复用,继续 │ │
│ │ B vs B → key 相同,type 相同 → 复用,继续 │ │
│ │ C vs E → key 不同 → 停止遍历 │ │
│ │ │ │
│ │ 结果:A、B 复用,指针停在 C 和 E 的位置 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 第二阶段:新节点用完了? │ │
│ │ │ │
│ │ 如果新节点列表遍历完了,删除剩余的旧节点 │ │
│ │ 本例:新节点还有 [E, F],跳过此阶段 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 第三阶段:旧节点用完了? │ │
│ │ │ │
│ │ 如果旧节点列表遍历完了,创建剩余的新节点 │ │
│ │ 本例:旧节点还有 [C, D],跳过此阶段 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 第四阶段:新旧节点都没用完,处理移动和新增 │ │
│ │ │ │
│ │ 1. 把剩余旧节点存入 Map(以 key 为索引) │ │
│ │ existingChildren = { C: fiberC, D: fiberD } │ │
│ │ │ │
│ │ 2. 遍历剩余新节点 [E, F],在 Map 中查找 │ │
│ │ E → Map 中没有 → 创建新 Fiber │ │
│ │ F → Map 中没有 → 创建新 Fiber │ │
│ │ │ │
│ │ 3. Map 中剩余的节点标记删除 │ │
│ │ C、D → 标记 Deletion │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 最终结果:复用 A、B,删除 C、D,创建 E、F │
│ │
└─────────────────────────────────────────────────────────────────┘
更复杂的例子(节点移动):
┌─────────────────────────────────────────────────────────────────┐
│ 节点移动的 Diff 过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧子节点: [A, B, C, D] │
│ 新子节点: [A, C, B, D] (B 和 C 交换了位置) │
│ │
│ 第一阶段:从左往右遍历 │
│ A vs A → 相同,复用 │
│ B vs C → key 不同,停止 │
│ │
│ 第四阶段: │
│ 1. 剩余旧节点存入 Map: { B: fiberB, C: fiberC, D: fiberD } │
│ │
│ 2. 遍历新节点 [C, B, D]: │
│ C → Map 中找到 → 复用,标记 Placement(移动) │
│ B → Map 中找到 → 复用,标记 Placement(移动) │
│ D → Map 中找到 → 复用,位置不变 │
│ │
│ 3. Map 清空,无节点需要删除 │
│ │
│ 最终结果:A 不变,C 和 B 移动位置,D 不变 │
│ │
└─────────────────────────────────────────────────────────────────┘
Q5: 为什么不能用 index 作为 key?
**问题场景:**在列表开头插入新元素
┌─────────────────────────────────────────────────────────────────┐
│ 使用 index 作为 key 的问题 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧列表(用 index 作为 key): │
│ ┌─────────────────────────────────────────┐ │
│ │ <li key=0>Apple</li> │ │
│ │ <li key=1>Banana</li> │ │
│ │ <li key=2>Cherry</li> │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 新列表(在开头插入 Durian): │
│ ┌─────────────────────────────────────────┐ │
│ │ <li key=0>Durian</li> ← 新元素 │ │
│ │ <li key=1>Apple</li> │ │
│ │ <li key=2>Banana</li> │ │
│ │ <li key=3>Cherry</li> │ │
│ └─────────────────────────────────────────┘ │
│ │
│ React 的理解(基于 key 匹配): │
│ ┌─────────────────────────────────────────┐ │
│ │ key=0: Apple → Durian (内容变了!) │ │
│ │ key=1: Banana → Apple (内容变了!) │ │
│ │ key=2: Cherry → Banana (内容变了!) │ │
│ │ key=3: (无) → Cherry (新增节点) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 结果:所有节点都被认为是"内容变了",全部重新渲染! │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 使用唯一 id 作为 key │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧列表: │
│ <li key="apple">Apple</li> │
│ <li key="banana">Banana</li> │
│ <li key="cherry">Cherry</li> │
│ │
│ 新列表: │
│ <li key="durian">Durian</li> ← 新元素 │
│ <li key="apple">Apple</li> ← 复用 │
│ <li key="banana">Banana</li> ← 复用 │
│ <li key="cherry">Cherry</li> ← 复用 │
│ │
│ 结果:只创建一个新节点,其他全部复用! │
│ │
└─────────────────────────────────────────────────────────────────┘
使用 index 作为 key 的危害:
- 性能问题:本应复用的节点被重新创建
- 状态错乱:如果列表项有输入框等状态,状态会错位到错误的节点
- 动画异常:动画可能应用到错误的元素上
Q6: 图解一个完整的列表更新 Diff 过程
**场景:**一个待办事项列表,用户完成了第一个任务,并添加了一个新任务。
┌─────────────────────────────────────────────────────────────────┐
│ 完整 Diff 过程示例 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 旧列表(待办事项): │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ id: 1, text: "学习 React", done: false │ │
│ │ id: 2, text: "写代码", done: false │ │
│ │ id: 3, text: "提交 PR", done: false │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ 新列表(用户操作后): │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ id: 1, text: "学习 React", done: true ← 状态变化 │ │
│ │ id: 2, text: "写代码", done: false │ │
│ │ id: 3, text: "提交 PR", done: false │ │
│ │ id: 4, text: "Code Review", done: false ← 新增 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 第一阶段 - 从左往右同步遍历 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 旧 key=1 vs 新 key=1 │ │
│ │ → key 相同,type 相同 │ │
│ │ → 复用 Fiber,标记 Update(props 变化:done: true) │ │
│ │ → 继续遍历 │ │
│ │ │ │
│ │ 旧 key=2 vs 新 key=2 │ │
│ │ → key 相同,type 相同 │ │
│ │ → 复用 Fiber,无 Update │ │
│ │ → 继续遍历 │ │
│ │ │ │
│ │ 旧 key=3 vs 新 key=3 │ │
│ │ → key 相同,type 相同 │ │
│ │ → 复用 Fiber,无 Update │ │
│ │ → 继续遍历 │ │
│ │ │ │
│ │ 旧节点用完,新节点还有 key=4 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 第二阶段 - 新节点用完了? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 新节点还有剩余,跳过此阶段 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: 第三阶段 - 旧节点用完了? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 旧节点已用完,新节点还有 [key=4] │ │
│ │ → 创建新 Fiber,标记 Placement │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Step 4: 第四阶段 - 处理移动和新增 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 不需要,前三个阶段已完成所有处理 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 最终 EffectList: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Fiber(key=1): Update → 更新 DOM 属性 │ │
│ │ Fiber(key=4): Placement → 插入新 DOM 节点 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
模块四:useState / useReducer 原理
📍 执行位置:Render 阶段执行组件函数时,Hooks 被调用,状态存入 Fiber.memoizedState
Q1: useState 的实现原理是什么?
核心原理: useState 是一个"魔法函数",它能够"记住"状态,是因为状态存储在 Fiber 节点的 memoizedState 链表中。
┌─────────────────────────────────────────────────────────────────┐
│ useState 工作原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 首次渲染(mount): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 创建 Hook 对象 │ │
│ │ 2. 初始化 memoizedState = initialState │ │
│ │ 3. 创建 updateQueue(存放 setState 的更新) │ │
│ │ 4. 将 Hook 添加到 Fiber.memoizedState 链表 │ │
│ │ 5. 返回 [state, dispatchSetState] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 更新渲染(update): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 从 Fiber.memoizedState 链表中取出对应的 Hook │ │
│ │ 2. 遍历 updateQueue,计算新 state │ │
│ │ newState = baseState + update1 + update2 + ... │ │
│ │ 3. 更新 Hook.memoizedState = newState │ │
│ │ 4. 返回 [newState, dispatchSetState] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
// 简化版 useState 实现(帮助理解原理)
let workInProgressHook = null; // 当前正在处理的 Hook
let currentlyRenderingFiber = null; // 当前渲染的 Fiber
function useState(initialState) {
// 获取或创建 Hook
const hook = mountWorkInProgressHook();
if (hook.memoizedState === null) {
// 首次渲染:初始化状态
hook.memoizedState = initialState;
hook.queue = { pending: null }; // 更新队列
} else {
// 更新渲染:处理更新队列
const queue = hook.queue;
let state = hook.memoizedState;
// 遍历所有待处理的更新
if (queue.pending) {
let update = queue.pending;
do {
state = typeof update.action === 'function'
? update.action(state)
: update.action;
update = update.next;
} while (update !== queue.pending);
hook.memoizedState = state;
queue.pending = null;
}
}
// 返回状态和 setter
return [hook.memoizedState, dispatchSetState.bind(null, hook.queue)];
}
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
queue: null,
next: null
};
// 添加到链表
if (workInProgressHook === null) {
// 第一个 Hook,存入 Fiber.memoizedState
currentlyRenderingFiber.memoizedState = hook;
} else {
// 后续 Hook,添加到链表末尾
workInProgressHook.next = hook;
}
workInProgressHook = hook;
return hook;
}
function dispatchSetState(queue, action) {
// 创建更新对象
const update = { action, next: null };
// 添加到更新队列(环形链表)
if (queue.pending === null) {
update.next = update; // 自己指向自己
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
// 触发调度更新
scheduleUpdateOnFiber(currentlyRenderingFiber);
}
Q2: setState 的批处理机制是怎样的?
批处理是指多个 setState 调用合并成一次渲染。
┌─────────────────────────────────────────────────────────────────┐
│ 批处理机制 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ React 17 及之前(自动批处理仅限于 React 事件处理器): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ function handleClick() { │ │
│ │ setCount(c => c + 1); // 不渲染 │ │
│ │ setCount(c => c + 1); // 不渲染 │ │
│ │ setCount(c => c + 1); // 不渲染 │ │
│ │ } // 函数结束后,合并渲染一次 │ │
│ │ │ │
│ │ // 但是在 setTimeout 中不会批处理: │ │
│ │ setTimeout(() => { │ │
│ │ setCount(c => c + 1); // 渲染一次 │ │
│ │ setCount(c => c + 1); // 渲染一次 │ │
│ │ }, 0); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ React 18(自动批处理所有场景): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ // 在任何地方都会批处理 │ │
│ │ setTimeout(() => { │ │
│ │ setCount(c => c + 1); // 不渲染 │ │
│ │ setCount(c => c + 1); // 不渲染 │ │
│ │ }, 0); // 合并渲染一次 │ │
│ │ │ │
│ │ // 如果需要立即渲染,使用 flushSync │ │
│ │ import { flushSync } from 'react-dom'; │ │
│ │ flushSync(() => { │ │
│ │ setCount(c => c + 1); // 立即渲染 │ │
│ │ }); │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
批处理的实现原理:
- React 维护一个
isBatchingUpdates标志 - 当标志为 true 时,setState 只是把更新加入队列,不触发渲染
- 当标志变为 false 时,批量处理所有更新,触发一次渲染
Q3: useReducer 和 useState 的关系?
真相: useState 内部就是用 useReducer 实现的!
// useState 的内部实现
function useState(initialState) {
return useReducer(
basicStateReducer, // reducer 函数
initialState
);
}
function basicStateReducer(state, action) {
// action 可能是新值,也可能是更新函数
return typeof action === 'function' ? action(state) : action;
}
// useReducer 实现
function useReducer(reducer, initialArg) {
const hook = mountWorkInProgressHook();
if (hook.memoizedState === null) {
hook.memoizedState = initialArg;
}
const dispatch = dispatchAction.bind(null, hook.queue, reducer);
return [hook.memoizedState, dispatch];
}
何时使用 useReducer?
- 状态逻辑复杂,涉及多个子值
- 下一个状态依赖于前一个状态
- 需要触发深更新的场景(通过 dispatch 传递)
模块五:useEffect / useLayoutEffect 原理
📍 执行位置:Render 阶段收集 Effect,Commit 阶段执行 Effect
Q1: useEffect 的执行时机是什么?
核心: useEffect 在 Commit 阶段完成后异步执行,不会阻塞浏览器绘制。
┌─────────────────────────────────────────────────────────────────┐
│ useEffect 执行时机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 时间线(从上到下) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render 阶段 │ │
│ │ - 执行组件函数 │ │
│ │ - useEffect 调用时,只是把 Effect 收集到 Hook 中 │ │
│ │ - 不执行 Effect 回调 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Commit 阶段 │ │
│ │ - beforeMutation: 读取 DOM 状态(如 getSnapshot) │ │
│ │ - mutation: 操作 DOM │ │
│ │ - layout: 执行 useLayoutEffect │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 浏览器绘制(Paint) │ │
│ │ - 用户看到新 UI │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ useEffect 执行(异步,通过 Scheduler 调度) │ │
│ │ - 不阻塞绘制 │ │
│ │ - 可能在下一帧执行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: useLayoutEffect 和 useEffect 的区别?
| useEffect | useLayoutEffect |
|---|---|
| Commit 阶段完成后异步执行 | Commit 阶段同步执行 |
| 在浏览器绘制之后执行 | 在浏览器绘制之前执行 |
| 不阻塞页面渲染 | 阻塞页面渲染 |
| 适合:数据获取、订阅、日志 | 适合:读取 DOM 布局、同步更新 |
┌─────────────────────────────────────────────────────────────────┐
│ useEffect vs useLayoutEffect │
├─────────────────────────────────────────────────────────────────┤
│ │
│ useEffect 的执行顺序: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Render │──▶│ Commit │──▶│ Paint │──▶│ Effect │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ 异步执行 │
│ │
│ useLayoutEffect 的执行顺序: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Render │──▶│ Commit │──▶│LayoutEff│──▶│ Paint │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ 同步阻塞 │
│ │
│ useLayoutEffect 适合的场景: │
│ - 测量 DOM 元素尺寸/位置 │
│ - 防止闪烁(如 tooltip 定位) │
│ - 同步更新 DOM │
│ │
└─────────────────────────────────────────────────────────────────┘
Q3: cleanup 函数的执行时机?
cleanup 函数在下次 Effect 执行前调用,用于清理上一次 Effect 的副作用。
┌─────────────────────────────────────────────────────────────────┐
│ cleanup 执行时机 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 组件生命周期: │
│ │
│ 挂载 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render → Commit → Paint → 执行 Effect │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 更新(依赖变化) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render → Commit → Paint → 执行 cleanup → 执行 Effect │ │
│ │ ↑ │ │
│ │ 先清理上一次 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 卸载 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 执行 cleanup → 组件销毁 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
// 典型用法:订阅/取消订阅
useEffect(() => {
const subscription = props.source.subscribe();
return () => { // cleanup 函数
subscription.unsubscribe();
};
}, [props.source]);
// 执行顺序:
// 1. 挂载时:执行 Effect(订阅)
// 2. 更新时:执行 cleanup(取消订阅)→ 执行 Effect(重新订阅)
// 3. 卸载时:执行 cleanup(取消订阅)
Q4: 手写简化版 useEffect
// 简化版 useEffect 实现
function useEffect(create, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (hook.memoizedState !== null) {
// 更新阶段:比较依赖
const prevEffect = hook.memoizedState;
if (areHookInputsEqual(nextDeps, prevEffect.deps)) {
// 依赖没变,跳过
return;
}
}
// 创建 Effect 对象
const effect = {
tag: HookEffect, // 标记为 Effect
create, // Effect 回调
destroy: undefined, // cleanup 函数(执行后填充)
deps: nextDeps, // 依赖数组
next: null // 链表下一个
};
hook.memoizedState = effect;
// 将 Effect 添加到 Fiber.updateQueue
pushEffect(currentlyRenderingFiber, effect);
}
// 依赖比较
function areHookInputsEqual(nextDeps, prevDeps) {
if (prevDeps === null) return false;
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
// Commit 阶段执行 Effect
function commitEffects(fiber) {
const updateQueue = fiber.updateQueue;
let effect = updateQueue.firstEffect;
while (effect !== null) {
// 先执行 cleanup
if (effect.destroy) {
effect.destroy();
}
// 再执行 create,保存返回的 cleanup
const destroy = effect.create();
effect.destroy = destroy;
effect = effect.next;
}
}
模块六:useMemo / useCallback 原理
📍 执行位置:Render 阶段执行,用于缓存计算结果和函数引用
Q1: useMemo 和 useCallback 的区别?
| useMemo | useCallback |
|---|---|
| 缓存计算结果 | 缓存函数引用 |
const result = useMemo(() => heavyCalculation(data), [data]); | const handleClick = useCallback(() => doSomething(id), [id]); |
| 依赖变化时重新计算 | 依赖变化时返回新函数 |
// useMemo:缓存计算结果
const result = useMemo(() => {
return heavyCalculation(data);
}, [data]);
// useCallback:缓存函数引用
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
本质关系:
useCallback(fn, deps)等价于useMemo(() => fn, deps)
Q2: useMemo 和 React.memo 的配合使用?
问题场景: 父组件传递 props 给 React.memo 包裹的子组件,但 props 中的函数每次都是新引用。
// 问题代码:React.memo 失效
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都创建新函数,导致 Child 重新渲染
const handleClick = () => {
console.log('clicked');
};
return (
<>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<Child onClick={handleClick} />
</>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click me</button>;
});
// 解决方案:使用 useCallback
function Parent() {
const [count, setCount] = useState(0);
// 缓存函数引用,只有依赖变化时才创建新函数
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // 空依赖,永远返回同一个函数
return <Child onClick={handleClick} />;
}
Q3: 手写 useMemo 和 useCallback
// 简化版 useMemo
function useMemo(factory, deps) {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
if (hook.memoizedState !== null) {
// 更新阶段
const prevState = hook.memoizedState;
if (areHookInputsEqual(nextDeps, prevState.deps)) {
// 依赖没变,返回缓存的结果
return prevState.value;
}
}
// 首次渲染或依赖变化,重新计算
const value = factory();
hook.memoizedState = { value, deps: nextDeps };
return value;
}
// 简化版 useCallback
function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
模块七:useRef 原理
📍 执行位置:Render 阶段执行,存储在 Fiber.memoizedState 中,修改不触发渲染
Q1: useRef 和 useState 的区别?
| useRef | useState |
|---|---|
| 存储在 Fiber 中,修改不触发渲染 | 存储在 Fiber 中,修改触发渲染 |
返回一个可变对象 { current: value } | 返回状态值和 setter |
| 整个组件生命周期保持不变 | 状态变化会触发组件重新渲染 |
| 适合:DOM 引用、存储任意可变值 | 适合:需要驱动 UI 更新的数据 |
┌─────────────────────────────────────────────────────────────────┐
│ useRef vs useState │
├─────────────────────────────────────────────────────────────────┤
│ │
│ useRef: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ const countRef = useRef(0); │ │
│ │ │ │
│ │ countRef.current = 5; // 直接修改,不触发渲染 │ │
│ │ console.log(countRef.current); // 5 │ │
│ │ │ │
│ │ // 组件重新渲染时,countRef 仍然是同一个对象 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ useState: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ const [count, setCount] = useState(0); │ │
│ │ │ │
│ │ setCount(5); // 触发渲染 │ │
│ │ // count = 5(渲染后) │ │
│ │ │ │
│ │ // 每次渲染,count 是新的值 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: useRef 的常见使用场景?
// 1. 访问 DOM 元素
function InputWithFocus() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current.focus(); // 直接操作 DOM
};
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus</button>
</>
);
}
// 2. 存储不触发渲染的值(如上一次的值)
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // 更新 ref,不触发渲染
}, [value]);
return ref.current; // 返回上一次的值
}
// 3. 存储定时器 ID
function Timer() {
const timerRef = useRef(null);
useEffect(() => {
timerRef.current = setInterval(() => {
// ...
}, 1000);
return () => clearInterval(timerRef.current);
}, []);
return <div>Timer</div>;
}
// 4. 解决闭包陷阱
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 保持最新
}, [count]);
const handleAlert = () => {
setTimeout(() => {
alert(countRef.current); // 总是显示最新的 count
}, 3000);
};
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<button onClick={handleAlert}>Alert after 3s</button>
</>
);
}
模块八:自定义 Hook + Hooks 规则
📍 执行位置:自定义 Hook 是复用状态逻辑的方式,本质是函数组合
Q1: 自定义 Hook 的原理是什么?
自定义 Hook 本质是一个函数,它内部调用其他 Hooks。每次组件调用自定义 Hook,都会创建独立的 Hook 链表节点。
┌─────────────────────────────────────────────────────────────────┐
│ 自定义 Hook 原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ function useWindowSize() { │
│ const [size, setSize] = useState({ width: 0, height: 0 });│
│ useEffect(() => { ... }, []); │
│ return size; │
│ } │
│ │
│ function ComponentA() { │
│ const size = useWindowSize(); // 创建 Hook 1, 2 │
│ } │
│ │
│ function ComponentB() { │
│ const size = useWindowSize(); // 创建 Hook 3, 4 │
│ } │
│ │
│ Fiber A.memoizedState: Hook1 → Hook2 → null │
│ Fiber B.memoizedState: Hook3 → Hook4 → null │
│ │
│ 每个组件的 Hook 链表是独立的,状态互不影响 │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: 为什么 Hooks 不能在条件语句中使用?
原因: Hooks 通过链表顺序匹配状态
┌─────────────────────────────────────────────────────────────────┐
│ Hooks 规则:顺序一致性 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 正确用法: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ function Correct() { │ │
│ │ const [a, setA] = useState(0); // Hook 1 │ │
│ │ const [b, setB] = useState(0); // Hook 2 │ │
│ │ useEffect(() => {}, []); // Hook 3 │ │
│ │ } │ │
│ │ │ │
│ │ 首次渲染:Hook1 → Hook2 → Hook3 │ │
│ │ 更新渲染:Hook1 → Hook2 → Hook3(顺序一致,匹配正确) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 错误用法: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ function Wrong({ condition }) { │ │
│ │ const [a, setA] = useState(0); // Hook 1 │ │
│ │ │ │
│ │ if (condition) { │ │
│ │ const [b, setB] = useState(0); // Hook 2? │ │
│ │ } │ │
│ │ │ │
│ │ useEffect(() => {}, []); // Hook 3? │ │
│ │ } │ │
│ │ │ │
│ │ 首次渲染(condition=true): Hook1 → Hook2 → Hook3 │ │
│ │ 更新渲染(condition=false):Hook1 → Hook3 │ │
│ │ │ │
│ │ 问题:Hook3 匹配到了 Hook2 的状态!状态错乱! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Q3: 手写 useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// 设置定时器
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// cleanup:清除定时器
return () => clearTimeout(timer);
}, [value, delay]); // value 变化时重新设置定时器
return debouncedValue;
}
// 使用示例
function SearchInput() {
const [text, setText] = useState('');
const debouncedText = useDebounce(text, 500);
useEffect(() => {
// 只有 debouncedText 变化时才搜索
if (debouncedText) {
search(debouncedText);
}
}, [debouncedText]);
return (
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Search..."
/>
);
}
模块九:useContext 原理
📍 执行位置:Render 阶段,沿 Fiber 树向上查找最近的 Provider
Q1: useContext 如何获取 Context 值?
原理: useContext 从当前 Fiber 开始,沿 return 指针向上遍历,找到最近的 Provider,获取其 value。
┌─────────────────────────────────────────────────────────────────┐
│ useContext 查找过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ const ThemeContext = createContext('light'); │
│ │
│ function App() { │
│ return ( │
│ <ThemeContext.Provider value="dark"> │
│ <Header /> │
│ </ThemeContext.Provider> │
│ ); │
│ } │
│ │
│ function Header() { │
│ const theme = useContext(ThemeContext); // 'dark' │
│ } │
│ │
│ Fiber 树结构: │
│ │
│ App (Fiber) │
│ │ │
│ ▼ child │
│ ThemeContext.Provider (Fiber) │
│ └── dependencies: [{ context, value: 'dark' }] │
│ │ │
│ ▼ child │
│ Header (Fiber) │
│ └── useContext 触发向上查找 │
│ │ │
│ ▼ return │
│ 找到 Provider → 获取 value: 'dark' │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: Context 的性能问题和优化方案?
问题: 当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染,即使它们只用了 value 的一部分。
// 问题示例:所有消费者都会重新渲染
const UserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Tom', age: 20 });
return (
<UserContext.Provider value={user}>
<UserName /> // 只用 user.name
<UserAge /> // 只用 user.age
</UserContext.Provider>
);
}
// 优化方案 1:拆分 Context
const UserNameContext = createContext();
const UserAgeContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'Tom', age: 20 });
return (
<UserNameContext.Provider value={user.name}>
<UserAgeContext.Provider value={user.age}>
<UserName />
<UserAge />
</UserAgeContext.Provider>
</UserNameContext.Provider>
);
}
// 优化方案 2:useMemo 缓存 value
function App() {
const [user, setUser] = useState({ name: 'Tom', age: 20 });
// 只有 user 变化时才创建新对象
const value = useMemo(() => ({ user }), [user]);
return (
<UserContext.Provider value={value}>
{/* ... */}
</UserContext.Provider>
);
}
// 优化方案 3:选择器模式(类似 Redux 的 useSelector)
function useContextSelector(Context, selector) {
const contextValue = useContext(Context);
return useMemo(() => selector(contextValue), [contextValue, selector]);
}
// 使用
function UserName() {
const name = useContextSelector(UserContext, user => user.name);
return <span>{name}</span>;
}
模块十:Redux 原理详解
📍 执行位置:独立的状态管理库,与 React 通过 react-redux 绑定
Q1: Redux 三大原则是什么?
1. 单一数据源 整个应用的 state 存储在一个 store 中,方便调试和持久化。
2. State 是只读的 唯一改变 state 的方式是触发 action,确保所有修改都有迹可循。
3. 使用纯函数修改 Reducer 是纯函数,接收旧 state 和 action,返回新 state。
Q2: 手写 createStore
function createStore(reducer, preloadedState) {
let currentState = preloadedState;
let listeners = [];
// 获取当前状态
function getState() {
return currentState;
}
// 订阅状态变化
function subscribe(listener) {
listeners.push(listener);
// 返回取消订阅函数
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
// 派发 action
function dispatch(action) {
currentState = reducer(currentState, action);
// 通知所有订阅者
listeners.forEach(listener => listener());
return action;
}
// 初始化:派发一个随机 action,触发 reducer 返回初始 state
dispatch({ type: '@@INIT' });
return { getState, subscribe, dispatch };
}
// 使用示例
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const store = createStore(reducer);
store.subscribe(() => {
console.log('State updated:', store.getState());
});
store.dispatch({ type: 'INCREMENT' }); // State updated: { count: 1 }
Q3: 手写 combineReducers
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
for (const key of reducerKeys) {
const reducer = reducers[key];
const previousStateForKey = state[key];
// 调用每个子 reducer
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
// 检查是否有变化(浅比较)
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
// 有变化返回新 state,无变化返回原 state
return hasChanged ? nextState : state;
};
}
// 使用示例
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
settings: settingsReducer
});
// State 结构:
// {
// users: { ... },
// posts: { ... },
// settings: { ... }
// }
Q4: 中间件原理是什么?
中间件是对 dispatch 的增强,可以在 action 到达 reducer 之前/之后执行额外逻辑。
┌─────────────────────────────────────────────────────────────────┐
│ 中间件链式调用 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ action → middleware1 → middleware2 → middleware3 → reducer │
│ ↓ ↓ ↓ │
│ (日志) (异步处理) (错误处理) │
│ │
│ 洋葱模型: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ middleware1 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ middleware2 │ │ │
│ │ │ ┌─────────────────────────────────────────┐ │ │ │
│ │ │ │ middleware3 │ │ │ │
│ │ │ │ ┌─────────────────────────────────┐ │ │ │ │
│ │ │ │ │ reducer │ │ │ │ │
│ │ │ │ └─────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
// applyMiddleware 实现
function applyMiddleware(...middlewares) {
return (createStore) => (reducer) => {
const store = createStore(reducer);
let dispatch = store.dispatch;
// 给中间件提供 API
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
// 初始化中间件
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 组合中间件:dispatch = middleware1(middleware2(middleware3(dispatch)))
dispatch = chain.reduceRight(
(next, middleware) => middleware(next),
store.dispatch
);
return { ...store, dispatch };
};
}
// 日志中间件
const logger = store => next => action => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
return result;
};
// thunk 中间件(处理异步 action)
const thunk = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
Q5: react-redux 的 useSelector / useDispatch 原理?
// 简化版 react-redux 实现
const Context = createContext(null);
// Provider:将 store 放入 Context
function Provider({ store, children }) {
return (
<Context.Provider value={store}>
{children}
</Context.Provider>
);
}
// useDispatch:返回 store.dispatch
function useDispatch() {
const store = useContext(Context);
return store.dispatch;
}
// useSelector:订阅 store 变化,选择部分 state
function useSelector(selector, equalityFn = Object.is) {
const store = useContext(Context);
const [, forceUpdate] = useReducer(c => c + 1, 0);
// 使用 ref 存储上一次的选择结果
const selectedRef = useRef();
const selected = selector(store.getState());
useEffect(() => {
const unsubscribe = store.subscribe(() => {
const newSelected = selector(store.getState());
// 只有选择的部分变化时才重新渲染
if (!equalityFn(selectedRef.current, newSelected)) {
selectedRef.current = newSelected;
forceUpdate();
}
});
return unsubscribe;
}, [store, selector]);
return selected;
}
模块十一:React 18 并发特性
📍 执行位置:Scheduler 阶段的增强,支持优先级调度和可中断渲染
Q1: 什么是并发模式?
并发模式允许 React 同时准备多个版本的 UI,根据优先级决定显示哪个版本。
┌─────────────────────────────────────────────────────────────────┐
│ 并发模式示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 传统模式(同步): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 开始渲染 ────────────────────────────────────▶ 完成 │ │
│ │ (阻塞,无法响应其他任务) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 并发模式: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 低优先级渲染 ──┬──▶ 暂停 ──▶ 用户输入 ──▶ 高优先级渲染 │ │
│ │ │ ↓ │ │
│ │ └──────────────────▶ 恢复渲染 ──▶ 完成 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 特点: │
│ - 渲染可中断、可恢复 │
│ - 高优先级任务可以打断低优先级任务 │
│ - 用户感知更流畅 │
│ │
└─────────────────────────────────────────────────────────────────┘
Q2: Lane 优先级模型是什么?
Lane是 React 18 的优先级模型,用 31 位二进制表示不同优先级。
┌─────────────────────────────────────────────────────────────────┐
│ Lane 优先级模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 优先级从高到低: │
│ │
│ SyncLane (0000...0001) 同步,最高优先级 │
│ InputContinuousLane(0000...0010) 连续输入(拖拽、hover) │
│ DefaultLane (0000...0100) 默认优先级 │
│ TransitionLane (0000...1000) 过渡更新(useTransition) │
│ IdleLane (0100...0000) 空闲时执行 │
│ │
│ 使用位运算判断优先级: │
│ - lanes & SyncLane !== 0 → 是否是同步任务 │
│ - lanes & TransitionLane !== 0 → 是否是过渡任务 │
│ │
│ 优势: │
│ - 位运算高效 │
│ - 可以同时表示多个优先级(多个位为 1) │
│ - 批量判断优先级 │
│ │
└─────────────────────────────────────────────────────────────────┘
Q3: useTransition 和 useDeferredValue 的区别?
| useTransition | useDeferredValue |
|---|---|
| 将状态更新标记为低优先级 | 延迟某个值的更新 |
const [isPending, startTransition] = useTransition(); | const deferredQuery = useDeferredValue(query); |
startTransition(() => { setSearchQuery(input); }); | deferredQuery 滞后于 query |
| 适合:主动标记某些更新为低优先级 | 适合:被动延迟某个值 |
// useTransition 示例:搜索输入
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 输入框更新:高优先级,立即响应
setQuery(e.target.value);
// 搜索结果更新:低优先级,可延迟
startTransition(() => {
setSearchResults(search(e.target.value));
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <span>Loading...</span>}
<Results results={searchResults} />
</>
);
}
Q4: Suspense 原理是什么?
Suspense允许组件"等待"某些东西(如数据加载),在等待期间显示 fallback。
┌─────────────────────────────────────────────────────────────────┐
│ Suspense 工作流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ <Suspense fallback={<Loading />}> │
│ <DataComponent /> │
│ </Suspense> │
│ │
│ 执行流程: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 渲染 DataComponent │ │
│ │ 2. DataComponent 发起数据请求,数据未就绪 │ │
│ │ 3. 抛出 Promise(throw promise) │ │
│ │ 4. Suspense 捕获 Promise,显示 fallback │ │
│ │ 5. Promise resolve,React 重试渲染 │ │
│ │ 6. 数据就绪,渲染真实内容 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 关键:组件可以 throw Promise,React 会"暂停"渲染 │
│ │
└─────────────────────────────────────────────────────────────────┘
// Suspense 数据获取示例
function DataComponent() {
// 如果数据未就绪,throw Promise
const data = use(fetchData()); // React 19 的 use API
return <div>{data}</div>;
}
// 简化版 use 实现
function use(promise) {
if (promise.status === 'fulfilled') {
return promise.value;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise; // 抛出 Promise,被 Suspense 捕获
} else {
promise.status = 'pending';
promise.then(
result => { promise.status = 'fulfilled'; promise.value = result; },
error => { promise.status = 'rejected'; promise.reason = error; }
);
throw promise;
}
}
模块十二:性能优化专题
📍 执行位置:贯穿整个渲染流程,减少不必要的计算和渲染
Q1: React.memo 的原理和使用场景?
React.memo是一个高阶组件,对组件进行浅比较,props 不变则跳过渲染。
// React.memo 原理
function memo(Component, arePropsEqual = shallowEqual) {
function MemoComponent(props) {
// 检查 props 是否变化
if (
MemoComponent.lastProps &&
arePropsEqual(props, MemoComponent.lastProps)
) {
// props 没变,返回缓存的 JSX
return MemoComponent.lastResult;
}
// props 变了,重新渲染
MemoComponent.lastProps = props;
MemoComponent.lastResult = <Component {...props} />;
return MemoComponent.lastResult;
}
return MemoComponent;
}
// 使用示例
const ExpensiveComponent = React.memo(({ data }) => {
// 复杂计算...
return <div>{data}</div>;
});
// 自定义比较函数
const MyComponent = React.memo(
({ user, onClick }) => { /* ... */ },
(prevProps, nextProps) => {
// 返回 true 表示相等,跳过渲染
return prevProps.user.id === nextProps.user.id;
}
);
注意事项:
- React.memo 只比较 props,不比较 state
- 如果 props 包含函数/对象,需要配合 useCallback/useMemo
- 不要过度使用,浅比较本身也有开销
Q2: 虚拟列表原理是什么?
虚拟列表只渲染可视区域的元素,大幅减少 DOM 节点数量。
┌─────────────────────────────────────────────────────────────────┐
│ 虚拟列表原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 总数据:10000 条 │
│ 可视区域:10 条 │
│ 实际渲染:15 条(包含缓冲区) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ↑ 滚动容器 │ │
│ │ │ │ │
│ │ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ │ Item 0 (缓冲区,预渲染) │ │ │
│ │ │ │ Item 1 (缓冲区,预渲染) │ │ │
│ │ │ │ ───────────────────────────────────── │ │ │
│ │ │ │ Item 2 ┐ │ │ │
│ │ │ │ Item 3 │ 可视区域 │ │ │
│ │ │ │ ... │ (实际渲染) │ │ │
│ │ │ │ Item 11 ┘ │ │ │
│ │ │ │ ───────────────────────────────────── │ │ │
│ │ │ │ Item 12 (缓冲区,预渲染) │ │ │
│ │ │ │ Item 13 (缓冲区,预渲染) │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ↓ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 关键计算: │
│ - startIndex = Math.floor(scrollTop / itemHeight) - bufferSize│
│ - endIndex = startIndex + visibleCount + bufferSize * 2 │
│ - translateY = startIndex * itemHeight (偏移定位) │
│ │
└─────────────────────────────────────────────────────────────────┘
// 简化版虚拟列表
function VirtualList({ items, itemHeight, visibleCount }) {
const [scrollTop, setScrollTop] = useState(0);
// 计算可视区域的起始和结束索引
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2);
const endIndex = Math.min(
items.length,
startIndex + visibleCount + 4
);
// 可视区域的数据
const visibleItems = items.slice(startIndex, endIndex);
return (
<div
style={{ height: visibleCount * itemHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{
position: 'absolute',
top: (startIndex + index) * itemHeight,
height: itemHeight
}}
>
{item}
</div>
))}
</div>
</div>
);
}
Q3: React 性能优化手段总结?
Render 阶段优化
- React.memo:避免不必要的重渲染
- useMemo:缓存计算结果
- useCallback:缓存函数引用
- 虚拟列表:减少 DOM 节点
- 懒加载:React.lazy + Suspense
Commit 阶段优化
- useEffect vs useLayoutEffect:优先使用 useEffect
- 批量更新:React 18 自动批处理
- useTransition:降低更新优先级
状态管理优化
- 状态下沉:状态放到最近的共同父组件
- 状态提升:避免重复状态
- Context 拆分:避免不必要的消费
- 状态管理库:Redux/Zustand 等
代码层面优化
- 合理的 key:使用稳定的唯一标识
- 避免内联函数/对象:或用 useCallback/useMemo
- 代码分割:动态 import
总结:React 完整执行流程
React 完整执行流程图
┌─────────────────────────────────────────────────────────────────┐
│ React 完整执行流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 触发更新:setState / dispatch / props 变化 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Scheduler 阶段 │ │
│ │ - 创建更新任务 │ │
│ │ - 分配优先级(Lane) │ │
│ │ - 调度执行 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Render 阶段(可中断) │ │
│ │ - 创建 WorkInProgress Fiber 树 │ │
│ │ - 执行组件函数 │ │
│ │ - 调用 Hooks(useState/useEffect/...) │ │
│ │ - 状态存入 Fiber.memoizedState 链表 │ │
│ │ - Diff 算法比较新旧 Fiber │ │
│ │ - 标记副作用 flags │ │
│ │ - 生成 EffectList │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Commit 阶段(不可中断) │ │
│ │ - beforeMutation │ │
│ │ - 读取 DOM 状态 │ │
│ │ - mutation │ │
│ │ - 插入/更新/删除 DOM 节点 │ │
│ │ - layout │ │
│ │ - 执行 useLayoutEffect │ │
│ │ - 同步执行,阻塞绘制 │ │
│ │ - 切换 current 指针 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 浏览器绘制(Paint) │ │
│ │ - 用户看到新 UI │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ useEffect 执行(异步) │ │
│ │ - 通过 Scheduler 调度 │ │
│ │ - 不阻塞绘制 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
各模块之间的联系
┌─────────────────────────────────────────────────────────────────┐
│ 模块关系图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Scheduler ──────▶ Reconciler ──────▶ Renderer │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │ Fiber │ │ │
│ │ │ 架构 │ │ │
│ │ └─────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Diff │ │ Hooks │ │ │
│ │ │ 算法 │ │ 系统 │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ │ │ │ │
│ │ │ ┌────────────┼────────────┐ │
│ │ │ ▼ ▼ ▼ │
│ │ │ useState useEffect useMemo │
│ │ │ useReducer useLayout useCallback │
│ │ │ Effect useRef │
│ │ │ useContext │
│ │ │ │
│ │ ▼ │
│ │ 状态存入 Fiber.memoizedState │
│ │ │
│ ▼ │
│ Lane 优先级模型(React 18) │
│ useTransition / useDeferredValue │
│ │
└─────────────────────────────────────────────────────────────────┘
高频手写题清单
Hooks 系列
- 🔴 高频 手写 useState
- 🔴 高频 手写 useEffect
- 🟡 中等 手写 useReducer
- 🟡 中等 手写 useMemo / useCallback
- 🟢 基础 手写 useRef
- 🟡 中等 手写 useDebounce
- 🟡 中等 手写 useThrottle
- 🔴 高频 手写 usePrevious
Redux 系列
- 🔴 高频 手写 createStore
- 🔴 高频 手写 combineReducers
- 🔴 高频 手写 applyMiddleware
- 🟡 中等 手写 bindActionCreators
- 🟡 中等 手写 useSelector / useDispatch
工具函数
- 🟢 基础 手写 shallowEqual
- 🟡 中等 手写 deepClone
- 🟡 中等 手写 debounce / throttle