React 面试完全指南(个人专用-应付面试)

5 阅读27分钟

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 有三层含义:

  1. 架构层面:React 16 采用的新架构,支持可中断渲染
  2. 数据结构层面:每个 Fiber 节点对应一个组件或 DOM 元素
  3. 工作单元层面:每个 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 知道 AB 只是交换了位置          │
  │  无 key:React 会认为 A 变成了 BB 变成了 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 Akey 相同,type 相同 → 复用,继续                 │  │
│  │  B vs Bkey 相同,type 相同 → 复用,继续                 │  │
│  │  C vs Ekey 不同 → 停止遍历                              │  │
│  │                                                           │  │
│  │  结果:AB 复用,指针停在 CE 的位置                    │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  第二阶段:新节点用完了?                                   │  │
│  │                                                           │  │
│  │  如果新节点列表遍历完了,删除剩余的旧节点                    │  │
│  │  本例:新节点还有 [E, F],跳过此阶段                        │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  第三阶段:旧节点用完了?                                   │  │
│  │                                                           │  │
│  │  如果旧节点列表遍历完了,创建剩余的新节点                    │  │
│  │  本例:旧节点还有 [C, D],跳过此阶段                        │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  第四阶段:新旧节点都没用完,处理移动和新增                  │  │
│  │                                                           │  │
│  │  1. 把剩余旧节点存入 Map(以 key 为索引)                   │  │
│  │     existingChildren = { C: fiberC, D: fiberD }           │  │
│  │                                                           │  │
│  │  2. 遍历剩余新节点 [E, F],在 Map 中查找                    │  │
│  │     EMap 中没有 → 创建新 Fiber                          │  │
│  │     FMap 中没有 → 创建新 Fiber                          │  │
│  │                                                           │  │
│  │  3. Map 中剩余的节点标记删除                                │  │
│  │     CD → 标记 Deletion                                   │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  最终结果:复用 AB,删除 CD,创建 EF                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

更复杂的例子(节点移动):

┌─────────────────────────────────────────────────────────────────┐
│                    节点移动的 Diff 过程                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   旧子节点: [A, B, C, D]                                        │
│   新子节点: [A, C, B, D]BC 交换了位置)                  │
│                                                                 │
│  第一阶段:从左往右遍历                                          │
│  A vs A → 相同,复用                                            │
│  B vs Ckey 不同,停止                                        │
│                                                                 │
│  第四阶段:                                                      │
│  1. 剩余旧节点存入 Map: { B: fiberB, C: fiberC, D: fiberD }     │
│                                                                 │
│  2. 遍历新节点 [C, B, D]:                                       │
│     CMap 中找到 → 复用,标记 Placement(移动)               │
│     BMap 中找到 → 复用,标记 Placement(移动)               │
│     DMap 中找到 → 复用,位置不变                             │
│                                                                 │
│  3. Map 清空,无节点需要删除                                     │
│                                                                 │
│  最终结果:A 不变,CB 移动位置,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 的危害:

  1. 性能问题:本应复用的节点被重新创建
  2. 状态错乱:如果列表项有输入框等状态,状态会错位到错误的节点
  3. 动画异常:动画可能应用到错误的元素上

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 的区别?

useEffectuseLayoutEffect
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 的区别?

useMemouseCallback
缓存计算结果缓存函数引用
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 的区别?

useRefuseState
存储在 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 的区别?

useTransitionuseDeferredValue
将状态更新标记为低优先级延迟某个值的更新
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. 抛出 Promisethrow promise)                         │  │
│   │  4. Suspense 捕获 Promise,显示 fallback                  │  │
│   │  5. Promise resolve,React 重试渲染                       │  │
│   │  6. 数据就绪,渲染真实内容                                 │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   关键:组件可以 throw PromiseReact"暂停"渲染              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
// 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