Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

React 生态 / React Ecosystem

1. React 生态知识体系 / React Ecosystem Knowledge System

2. React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow

3. Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency

4. 路由、表单与组件架构 / Routing, Forms, and Component Architecture

5. React 状态管理:Redux Toolkit 与 Zustand / React State Management: Redux Toolkit and Zustand

6. Next.js App Router:路由、渲染与工程边界 / Next.js App Router, Routing, Rendering, and Engineering Boundaries

7. Next.js 数据获取、缓存与变更 / Next.js Data Fetching, Caching, and Mutations

8. TanStack Query 与服务端状态管理 / TanStack Query and Server State Management

9. React 测试、性能与生产工程 / React Testing, Performance, and Production Engineering

本页目录

React 组件渲染、协调与数据流 / React Components, Rendering, and Data Flow ​

📅 创建时间:2026-07-28 🏷️ 标签:#React #Rendering #Fiber #Components #Concurrent 📚 前置知识:[[00-overview]]


📋 本章目标 ​

  • 理解 React 渲染的三个阶段:Render、Reconcile、Commit,以及它们各自做什么
  • 掌握 Fiber 架构的核心思想:可中断渲染、work loop、优先级调度
  • 牢记组件纯度的规则:渲染期间不能修改外部、不能发请求、不能操作 DOM
  • 理解 Props 只读、State 快照、单向数据流的深层含义
  • 掌握 key 的身份标识语义,而不是把它当性能优化技巧
  • 理解并发特性(Suspense、startTransition、useDeferredValue)的工作原理
  • 建立 memo/useMemo/useCallback 的决策框架,避免过早优化

第1部分:渲染、协调与提交 —— React 的三幕剧 ​

1.1 一次更新的完整旅程 ​

当组件首次挂载或状态更新时,React 不会直接操作 DOM。它先完整计算"UI 应该是什么样",然后只在必要的时候接触真实 DOM。这个过程分为三个严格的阶段:

┌─────────────────────────────────────────────────────────────┐
│              React 更新三阶段                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  触发源:setState / 父组件重渲染 / Context 变化 / 首次挂载    │
│     │                                                       │
│     ▼                                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │          阶段一:Render(渲染 / 计算)                │   │
│  │                                                     │   │
│  │  React 调用组件函数(或 class 的 render 方法)       │   │
│  │  传入当前 Props 和 State,产出新的 React Element 树  │   │
│  │                                                     │   │
│  │  function Greeting({ name }) {                      │   │
│  │    return <h1>Hello, {name}</h1>  ← 这就是渲染结果  │   │
│  │  }                                                  │   │
│  │                                                     │   │
│  │  关键:Render 阶段是纯净的 —— 没有副作用,不碰 DOM   │   │
│  │  在 Concurrent Mode 下,Render 可以被中断和丢弃      │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │        阶段二:Reconcile(协调 / 对比)              │   │
│  │                                                     │   │
│  │  将新生成的 Element 树与当前 Fiber 树逐节点对比      │   │
│  │  判断规则:                                          │   │
│  │    type 不同? → 整个子树重建                       │   │
│  │    type 相同? → 复用 DOM 节点,更新 Props          │   │
│  │    key 不同? → 视为不同身份,重建                  │   │
│  │                                                     │   │
│  │  输出是一个 effect list(需要执行的 DOM 操作清单)   │   │
│  │  这个阶段不操作 DOM,只标记"哪里需要改"             │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │                                   │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │        阶段三:Commit(提交 / 应用)                 │   │
│  │                                                     │   │
│  │  不可中断(同步执行),分为三个子阶段:               │   │
│  │                                                     │   │
│  │  BeforeMutation:读取当前 DOM 快照(getSnapshot)    │   │
│  │  Mutation:      执行 DOM 增删改、更新 ref           │   │
│  │  Layout:        执行 useLayoutEffect,浏览器绘制    │   │
│  │                  在 Mutation 之后、Paint 之前        │   │
│  │                                                     │   │
│  │  Commit 完成后 → 浏览器绘制 → 执行 useEffect        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

1.2 渲染不等于 DOM 更新 ​

这是 React 初学者最容易产生的误解。React 可以多次调用你的组件函数(Render),但如果在 Reconcile 阶段发现新旧输出完全一致,Commit 阶段会跳过该节点 —— 对应 DOM 没有任何变化。

tsx
// 父组件重渲染时,Child 也会被重新调用(Render)
// 但如果它的 props 没变,Commit 阶段会跳过它的 DOM
function Parent() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child name="Alice" /> {/* 每次 count 变,Child 都重渲染 */}
    </div>
  )
}

function Child({ name }: { name: string }) {
  console.log('Child rendered') // 每次都会打印
  return <p>{name}</p>           // 但 DOM 不会更新(props 没变)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

1.3 同步渲染 vs 并发渲染 ​

┌─────────────────────────────────────────────────────────────┐
│              同步渲染 vs 并发渲染                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  同步渲染(React 17 及之前默认):                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ [Render]━━━━━━━━━━━━━━━━━━━━━━━━[Commit]            │   │
│  │                                                     │   │
│  │  一次开始就必须一口气完成,中间不能被打断             │   │
│  │  大型更新会阻塞主线程,导致掉帧、输入卡顿             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  并发渲染(React 18+ Concurrent Mode):                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ [Render]····⏸····[Render]····⏸····[Commit]         │   │
│  │           ↑ 中断          ↑ 中断                    │   │
│  │                                                     │   │
│  │  React 可以在 Render 中途暂停,处理更高优先级的更新  │   │
│  │  (如用户输入),完成后再继续或丢弃当前渲染           │   │
│  │  Commit 阶段始终同步 —— 不能让用户看到半成品         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第2部分:Fiber 架构 —— React 的"可中断引擎" ​

2.1 为什么需要 Fiber ​

React 15 使用递归的 Stack Reconciler,遍历组件树的过程不可中断。当组件树很大时,一次递归可能需要 16ms 以上,导致浏览器无法在帧间隔内绘制,用户感知为卡顿。Fiber 架构(React 16+)把协调工作拆分成可中断的小单元,让 React 可以"暂停"渲染,把主线程还给浏览器。

2.2 Fiber Node 的本质 ​

每个组件实例、每个 DOM 元素在 React 内部都对应一个 Fiber node:

┌─────────────────────────────────────────────────────────────┐
│              一个 Fiber Node 的核心数据结构(概念模型)       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  type: 'div' | Greeting | null                       │   │
│  │  key:  null | 'user-42'                              │   │
│  │                                                      │   │
│  │  // 实例 / DOM 引用                                   │   │
│  │  stateNode: DOMElement | ComponentInstance           │   │
│  │                                                      │   │
│  │  // 树关系(Fiber 是链表树,不是递归树)              │   │
│  │  child:   Fiber | null    ← 第一个子节点             │   │
│  │  sibling: Fiber | null    ← 下一个兄弟节点           │   │
│  │  return:  Fiber | null    ← 父节点                   │   │
│  │                                                      │   │
│  │  // 双缓冲:当前屏幕 vs 正在构建                      │   │
│  │  alternate: Fiber | null   ← 另一棵树中的对应节点    │   │
│  │                                                      │   │
│  │  // 副作用标记                                         │   │
│  │  flags: Placement | Update | Deletion | ...          │   │
│  │  subtreeFlags: ...                                   │   │
│  │                                                      │   │
│  │  // 优先级                                             │   │
│  │  lanes: number     ← 本节点关联的更新优先级          │   │
│  │  childLanes: number ← 子树中的更新优先级             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Fiber 树的遍历方式(深度优先、可中断):                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │        App ← return                                 │   │
│  │        /  \                                         │   │
│  │   child sibling                                    │   │
│  │      /    \                                         │   │
│  │  Header  Main                                       │   │
│  │     |      |                                        │   │
│  │   child  child                                      │   │
│  │                                                     │   │
│  │  遍历顺序:App → Header → (回到 App) → Main         │   │
│  │  每个 Fiber 处理完就检查:还有时间吗?要不要暂停?    │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

2.3 Work Loop 与时间切片 ​

Fiber 的核心调度机制是一个循环,React 称之为 work loop:

┌─────────────────────────────────────────────────────────────┐
│              Work Loop:每 5ms 检查一次是否要交出控制权       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                                     │   │
│  │  function workLoop(deadline) {                      │   │
│  │    while (nextUnitOfWork &&                         │   │
│  │           deadline.timeRemaining() > 5) {           │   │
│  │      nextUnitOfWork = performUnitOfWork(            │   │
│  │        nextUnitOfWork                               │   │
│  │      )                                              │   │
│  │    }                                                │   │
│  │    if (nextUnitOfWork) {                            │   │
│  │      // 还有工作没做完,请求下一个空闲回调            │   │
│  │      requestIdleCallback(workLoop)                  │   │
│  │    }                                                │   │
│  │  }                                                  │   │
│  │                                                     │   │
│  │  performUnitOfWork(fiber):                           │   │
│  │    1. beginWork(fiber)     —— 协调当前节点          │   │
│  │    2. 深度优先找下一个 fiber                          │   │
│  │       → 先 child,没有则 sibling,                     │   │
│  │         都没有则向上 return 找叔叔                   │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  关键:React 实际上使用自己的 Scheduler 包实现时间切片,     │
│  而不是直接用 requestIdleCallback(后者兼容性差、优先级低)  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

2.4 Lane 模型 —— 优先级调度 ​

React 使用 31 位二进制数(lane)来表示更新的优先级。位运算使得"比较优先级""合并更新""判断是否包含某优先级"都是 O(1):

tsx
// Lane 的概念模型(简化)
// 数字越小优先级越高

// ┌─────────────────────────────────────────────────┐
// │ SyncLane            = 0b0000000000000000000001 │ → 同步(点击、输入)
// │ InputContinuousLane = 0b0000000000000000000100 │ → 连续输入
// │ DefaultLane         = 0b0000000000000000010000 │ → 默认(setState)
// │ TransitionLane      = 0b0000000000000010000000 │ → startTransition
// │ IdleLane            = 0b0010000000000000000000 │ → 空闲时执行
// └─────────────────────────────────────────────────┘

// 当高优先级更新到达时:
// 1. 检查当前正在渲染的 lanes 是否包含更高优先级的更新
// 2. 如果是,中断当前渲染,先处理高优先级更新
// 3. 被中断的低优先级渲染可以稍后恢复(或丢弃)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Lane 模型是 Concurrent Mode 的基础。它让 React 能够在"响应用户输入"和"渲染大量数据"之间做出取舍,始终让页面保持响应。


第3部分:组件必须保持纯净 ​

3.1 纯函数的定义 ​

在 React 中,一个组件是"纯净的"意味着:给定相同的 Props、State 和 Context,组件总是产出相同的 JSX。组件应该是 props → JSX 的确定性映射。

┌─────────────────────────────────────────────────────────────┐
│              纯组件 vs 不纯组件                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 纯组件:                                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ function Price({ amount, currency }: PriceProps) {  │   │
│  │   // 仅依赖输入,输出由输入唯一决定                  │   │
│  │   const formatted = new Intl.NumberFormat(          │   │
│  │     'zh-CN', { style: 'currency', currency }        │   │
│  │   ).format(amount)                                  │   │
│  │   return <output>{formatted}</output>               │   │
│  │ }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ❌ 不纯组件:                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ let counter = 0  // 模块级可变变量!                 │   │
│  │                                                     │   │
│  │ function ImpureBadge() {                            │   │
│  │   counter++  // 每次渲染都改变外部世界               │   │
│  │   return <span>渲染了 {counter} 次</span>           │   │
│  │ }  // 同样的 props,每次调用结果不同                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

3.2 渲染期间绝对不能做的事 ​

React 的 Render 阶段可以随时被中断、丢弃、重来。在并发模式下,React 甚至可能多次调用你的组件来检测副作用。因此在渲染期间:

┌─────────────────────────────────────────────────────────────┐
│              渲染期间 (Render Phase) 的禁令                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 修改外部变量或模块状态                                   │
│  ❌ 发起网络请求(fetch / axios)                            │
│  ❌ 直接操作 DOM(document.getElementById / innerHTML)      │
│  ❌ 调用 setState 触发新的渲染(可能导致无限循环)           │
│  ❌ 设置计时器(setTimeout / setInterval)                   │
│  ❌ 读写 localStorage / sessionStorage                      │
│  ❌ 修改传入的 Props 或 ref.current(在渲染输出前)          │
│                                                             │
│  这些操作应该在哪里执行?                                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ Event Handler   → 响应用户交互                       │   │
│  │ useEffect       → 同步外部系统(浏览器绘制后)       │   │
│  │ useLayoutEffect → 在浏览器绘制前同步执行             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

3.3 纯组件的好处 ​

  • 可预测:Props 出了结果一定对,调试时不需要排查外部状态。
  • 可并发:React 可以安全地中断和重试渲染,不会产生副作用。
  • 可测试:给输入验证输出,不需要 mock 任何全局变量。
  • Strict Mode 检查:React 18+ 在开发环境下会双重调用组件函数,暴露不纯的逻辑。

第4部分:Props 是只读输入 ​

4.1 单向数据流 ​

React 的数据流是严格单向的:父组件通过 Props 把数据"向下"传递,子组件通过回调函数把意图"向上"传达。

tsx
// ✅ 正确的单向数据流
type TodoItemProps = {
  todo: { id: string; title: string; done: boolean }
  onToggle: (id: string) => void
}

function TodoItem({ todo, onToggle }: TodoItemProps) {
  return (
    <label>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}  // 表达意图,不直接修改
      />
      {todo.title}
    </label>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.2 不要修改 Props ​

┌─────────────────────────────────────────────────────────────┐
│              为什么不能修改 Props                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ todo.done = true       // 直接修改 Props 对象            │
│  ❌ props.list.push(item)  // 修改 Props 中的数组            │
│  ❌ props.config.theme = 'dark' // 修改嵌套对象              │
│                                                             │
│  后果:                                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1. React 不知道数据变了 → 不会重新渲染              │   │
│  │ 2. 数据流变得无法追踪 → "这个值到底是谁改的?"      │   │
│  │ 3. 父组件持有的引用被破坏 → 后续对比失效            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  正确做法:子组件通过回调表达意图,父组件负责更新             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第5部分:State 是一次渲染的快照 ​

5.1 setState 请求下一次渲染 ​

这是 React 中最重要的心智模型之一。当你调用 setState 时,你当前闭包中的变量不会立即改变:

tsx
function Counter() {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)    // count = 0
    setCount(count + 1)    // count 仍然是 0!
    console.log(count)     // 打印 0,不是 1
  }

  // 点击一次,count 变成 1,不是 2
  // 因为两次 setCount(count + 1) 都读取的是当前渲染的快照值 0
}
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────┐
│              State 快照语义                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  渲染 1(count = 0)                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户点击按钮                                         │   │
│  │   setCount(0 + 1)  → React:下次渲染 count = 1      │   │
│  │   setCount(0 + 1)  → React:下次渲染 count = 1      │   │
│  │   console.log(0)   → 当前闭包中 count 仍是 0        │   │
│  │                                                     │   │
│  │  React 批处理这两次 setState,最终 count = 1         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  渲染 2(count = 1)                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 组件用新的 count = 1 重新执行                         │   │
│  │ 新的闭包中 count = 1,与上一次渲染完全隔离            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

5.2 函数式更新 ​

当新状态依赖旧状态时,使用函数式更新确保拿到最新的计算值:

tsx
// ❌ 基于当前快照值
setCount(count + 1)
setCount(count + 1)
// 结果:count 只增加 1

// ✅ 基于更新队列中的最新值
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// 结果:count 增加 2
// React 把这两次 update function 放入队列,按顺序执行
1
2
3
4
5
6
7
8
9
10

必须使用函数式更新的场景:

  • 在同一个事件处理中多次更新同一个 state
  • 在 setTimeout/Promise 回调中更新(这些回调捕获了旧的闭包值)
  • 在 useEffect 中更新,且依赖了该 state

5.3 批量更新(Batching) ​

React 18+ 默认对所有更新进行自动批处理:

┌─────────────────────────────────────────────────────────────┐
│              自动批处理(Automatic Batching)                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  React 17:只有事件处理中的更新会被批处理                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ setTimeout(() => {                                  │   │
│  │   setCount(c => c + 1)  // 触发渲染                  │   │
│  │   setFlag(true)         // 再次触发渲染              │   │
│  │   // React 17: 两次渲染!                            │   │
│  │ })                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  React 18+:所有更新都自动批处理                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ setTimeout(() => {                                  │   │
│  │   setCount(c => c + 1)                              │   │
│  │   setFlag(true)                                     │   │
│  │   // React 18: 一次渲染!                           │   │
│  │ })                                                  │   │
│  │                                                     │   │
│  │ // 如果确实需要强制同步刷新(极少见):              │   │
│  │ flushSync(() => setCount(c => c + 1))              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

第6部分:Key 的深层含义 ​

6.1 Key 是身份标识,不是性能技巧 ​

React 文档中经常说"key 帮助 React 识别哪些元素改变了",这容易让人误以为 key 是某种性能优化。实际上,key 是兄弟节点之间的唯一身份标识。没有 key(或使用错误的 key),React 会错判两个节点的身份关系。

┌─────────────────────────────────────────────────────────────┐
│              Key 决定"谁是谁"                                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景:列表重排序 [A, B, C] → [B, C, A]                      │
│                                                             │
│  没有 key(使用 index):                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ index=0: 原来是 A,现在是 B → React 认为:          │   │
│  │   "index=0 的节点还是同一个,只是内容变了"          │   │
│  │   更新 A 的 DOM 为 B 的内容                          │   │
│  │                                                     │   │
│  │ index=1: 原来是 B,现在是 C → 更新 DOM              │   │
│  │ index=2: 原来是 C,现在是 A → 更新 DOM              │   │
│  │                                                     │   │
│  │ 结果:每个节点都更新了 DOM,全部重建                 │   │
│  │ 如果有输入框,输入状态会错位到错误的行!             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  有正确的 key(基于业务 ID):                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ key='a': 之前在第0位,现在在第2位 → React 知道:    │   │
│  │   "节点 A 换了位置,但身份没变"                      │   │
│  │   只需移动 DOM 元素的位置,无需重建                  │   │
│  │                                                     │   │
│  │ key='b': 之前在第1位,现在在第0位 → 移位置          │   │
│  │ key='c': 之前在第2位,现在在第1位 → 移位置          │   │
│  │                                                     │   │
│  │ 结果:DOM 节点被移动而非重建,输入状态保持正确       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

6.2 为什么不能用 index ​

tsx
// ❌ 危险:使用 index 作为 key
{todos.map((todo, index) => (
  <TodoItem key={index} todo={todo} />
))}

// 当列表头部插入一项时:
// 之前: index=0→"买牛奶", index=1→"写代码"
// 之后: index=0→"取快递"(新), index=1→"买牛奶", index=2→"写代码"
// React 认为 index=0 的节点从"买牛奶"变成了"取快递"——内容更新
// index=1 的节点从"写代码"变成了"买牛奶"——内容更新
// 实际上只是插入了一项,其余没变!
// 如果有未受控的输入框,输入内容会错位
1
2
3
4
5
6
7
8
9
10
11
12

6.3 Key 只在当前列表层级有意义 ​

tsx
// key 不会作为 prop 传入组件
function TodoItem(props) {
  console.log(props.key) // undefined —— key 不是 prop
}

// 如果需要业务 ID 在组件内使用,单独传递:
{todos.map(todo => (
  <TodoItem key={todo.id} id={todo.id} todo={todo} />
))}

// 不同列表的 key 不需要全局唯一
<div>
  {users.map(u => <User key={u.id} ... />)}    {/* key="1" ~ "5" */}
  {posts.map(p => <Post key={p.id} ... />)}     {/* key="1" ~ "100" */}
  {/* 两个列表有重复 key 没关系,它们在不同父级下 */}
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第7部分:并发特性 ​

7.1 Suspense —— 让组件"等待" ​

Suspense 让 React 可以在组件还没准备好时显示 fallback,而不会阻塞整个页面:

tsx
import { Suspense, lazy } from 'react'

// 代码分割
const Dashboard = lazy(() => import('./Dashboard'))

// 数据获取(需要支持 Suspense 的数据库,如 TanStack Query 5+)
function App() {
  return (
    <Suspense fallback={<Loading skeleton />}>
      <Dashboard />
    </Suspense>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────────┐
│              Suspense 边界的工作原理                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                   App                                │   │
│  │  ┌───────────────────────────────────────────────┐  │   │
│  │  │  Suspense (fallback=<Spinner />)              │  │   │
│  │  │  ┌─────────────────────────────────────────┐ │  │   │
│  │  │  │  Dashboard (正在加载数据...)             │ │  │   │
│  │  │  │  ↓ 抛出 Promise                          │ │  │   │
│  │  │  │  React 捕获这个 Promise                   │ │  │   │
│  │  │  │  → Dashboard 不渲染                      │ │  │   │
│  │  │  │  → 显示 fallback: <Spinner />            │ │  │   │
│  │  │  │  → Promise resolve 后重试                 │ │  │   │
│  │  │  └─────────────────────────────────────────┘ │  │   │
│  │  └───────────────────────────────────────────────┘  │   │
│  │                                                     │  │
│  │  Suspense 之外的组件不受影响,正常渲染               │  │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  关键:Suspense 让"等待"变得声明式                          │
│  不需要 isLoading && <Spinner /> 的命令式条件判断           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

7.2 startTransition —— 标记非紧急更新 ​

tsx
import { useState, useTransition } from 'react'

function SearchPage() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    // 紧急更新:输入框必须立即响应
    setQuery(e.target.value)

    // 非紧急更新:搜索结果可以等一等
    startTransition(() => {
      setResults(search(e.target.value))
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <small>搜索中...</small>}
      <ResultList results={results} />
    </>
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────┐
│              startTransition 的优先级调度                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户快速输入 "react"(5个字符,5次 onChange):             │
│                                                             │
│  不使用 startTransition:                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ [渲染r][渲染re][渲染rea][渲染reac][渲染react]       │   │
│  │  每次都要等搜索结果渲染完才能处理下一个字符          │   │
│  │  输入框卡顿,用户体验差                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  使用 startTransition:                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 紧急 [r→e→a→c→t] 输入框始终紧跟键盘                 │   │
│  │ 非紧急 [搜索"r"]⏸→[搜索"re"]⏸→...→[搜索"react"]    │   │
│  │  中间的搜索被中断丢弃,只执行最后一次               │   │
│  │  输入框丝滑,搜索不阻塞                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

7.3 useDeferredValue —— 延迟某个值的更新 ​

useDeferredValue 是 startTransition 的"值"版本。当你无法控制 setState 的调用位置时(比如 props 来自第三方库),使用 useDeferredValue 来延迟某个值的同步:

tsx
function SearchResults({ query }: { query: string }) {
  // deferredQuery 会"滞后"于 query
  // 当 query 快速变化时,deferredQuery 保持在旧值
  // React 在空闲时才更新 deferredQuery
  const deferredQuery = useDeferredValue(query)

  // 使用 deferredQuery 渲染列表
  // 这样即使 query 频繁变化,列表重渲染也不会阻塞输入
  const results = useMemo(
    () => search(deferredQuery),
    [deferredQuery]
  )

  return <ResultList results={results} />
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

7.4 Strict Mode 的双重渲染 ​

React 18+ 的 Strict Mode 在开发环境下会双重调用以下内容:

  • 组件函数体(Render 阶段)
  • 状态更新函数(setCount(prev => ...) 中的 updater)
  • useState / useReducer 的初始值函数
  • useMemo / useCallback 的计算函数
┌─────────────────────────────────────────────────────────────┐
│              Strict Mode 双重调用                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  目的:在开发时暴露不纯的组件逻辑                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ function Counter() {                                │   │
│  │   console.log('rendering') // 开发模式下打印两次     │   │
│  │                                                     │   │
│  │   const [count, setCount] = useState(() => {        │   │
│  │     console.log('initializer') // 打印两次          │   │
│  │     return 0                                        │   │
│  │   })                                                │   │
│  │                                                     │   │
│  │   return <div>{count}</div>                         │   │
│  │ }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  这确保你的组件在并发模式下被中断后重新渲染时:              │
│  • 不会产生错误的副作用(如重复计数)                        │
│  • 不会依赖"渲染只执行一次"的假设                            │
│                                                             │
│  只在开发环境生效,生产构建不受影响                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

第8部分:memo / useMemo / useCallback 决策框架 ​

8.1 它们解决了什么问题 ​

┌─────────────────────────────────────────────────────────────┐
│              memo / useMemo / useCallback 职责               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  React.memo(Component)                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 对组件进行浅层 props 比较,跳过无变化的渲染          │   │
│  │                                                     │   │
│  │ 使用场景:                                           │   │
│  │ • 组件经常渲染但 props 很少变化                      │   │
│  │ • 组件的渲染开销很大(大型列表、图表、动画)         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  useMemo(fn, deps)                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 缓存计算结果,deps 不变时直接返回缓存值              │   │
│  │                                                     │   │
│  │ 使用场景:                                           │   │
│  │ • 计算开销大(filter + sort + map 大数组)          │   │
│  │ • 作为其他 Hook 的依赖,需要引用稳定性              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  useCallback(fn, deps)                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 缓存函数引用,deps 不变时返回同一个函数              │   │
│  │                                                     │   │
│  │ 使用场景:                                           │   │
│  │ • 传给 memo 子组件的回调,避免子组件不必要渲染      │   │
│  │ • 作为 useEffect 的依赖                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

8.2 决策树 —— 什么时候该用 ​

┌─────────────────────────────────────────────────────────────┐
│              memo / useMemo / useCallback 决策树             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  你的组件重渲染频繁吗?                                       │
│       │                                                     │
│       ├── 不频繁 → 什么都不要加,保持代码简洁               │
│       │                                                     │
│       └── 频繁 ↓                                            │
│           │                                                 │
│           ├── 子组件 props 没变但跟着渲染了?                │
│           │   └── 给子组件加 React.memo                     │
│           │       └── 如果传了内联对象/函数/JSX作为props    │
│           │           └── 用 useMemo / useCallback 包裹     │
│           │                                                 │
│           ├── 组件内有重计算(大数组 filter/sort/reduce)?  │
│           │   └── 用 useMemo 包裹计算结果                   │
│           │                                                 │
│           └── 回调函数作为 useEffect 的依赖?                │
│               └── 用 useCallback 保持引用稳定               │
│                                                             │
│  ⚠️ 常见误区:                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ ❌ 给所有组件加 memo  → 比较也有开销                 │   │
│  │ ❌ 所有值都 useMemo  → 内存中存了大量缓存值          │   │
│  │ ❌ 所有函数都 useCallback → 闭包捕获更多变量         │   │
│  │                                                     │   │
│  │ ✅ 先用 React DevTools Profiler 测量,再决策        │   │
│  │ ✅ 通常"提升状态"或"组件拆分"比 memo 更有效         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

8.3 比较开销 vs 重渲染开销 ​

tsx
// ❌ 过度优化:计算比渲染便宜多了
function UserName({ user }: { user: User }) {
  const fullName = useMemo(
    () => `${user.firstName} ${user.lastName}`,
    [user.firstName, user.lastName]
  )
  return <span>{fullName}</span>
}
// 字符串拼接的代价 < useMemo 的 deps 比较 + 闭包内存

// ✅ 合理使用:计算开销确实大
function FilteredList({ items, query }: Props) {
  const filtered = useMemo(() => {
    return items
      .filter(item => matchesQuery(item, query))
      .sort(byPriority)
      .map(withMetadata)
  }, [items, query])
  return <List items={filtered} />
}
// 对 10000 条记录的 filter+sort+map,useMemo 完全值得
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

第9部分:受控组件与非受控组件 ​

9.1 两种模式 ​

┌─────────────────────────────────────────────────────────────┐
│              受控 vs 非受控组件                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  受控组件(Controlled)                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 表单的值由 React State 驱动                          │   │
│  │                                                     │   │
│  │ function Form() {                                   │   │
│  │   const [email, setEmail] = useState('')            │   │
│  │   return (                                          │   │
│  │     <input                                         │   │
│  │       value={email}                                 │   │
│  │       onChange={e => setEmail(e.target.value)}      │   │
│  │     />                                              │   │
│  │   )                                                 │   │
│  │ }                                                   │   │
│  │                                                     │   │
│  │  React 是数据的唯一真实来源(Single Source of Truth)│   │
│  │  适合:需要实时校验、格式化、条件禁用提交按钮       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  非受控组件(Uncontrolled)                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ DOM 自己管理状态,React 只在需要时读取               │   │
│  │                                                     │   │
│  │ function Form() {                                   │   │
│  │   const inputRef = useRef<HTMLInputElement>(null)   │   │
│  │   function handleSubmit() {                         │   │
│  │     console.log(inputRef.current?.value)            │   │
│  │   }                                                 │   │
│  │   return (                                          │   │
│  │     <input defaultValue="" ref={inputRef} />        │   │
│  │   )                                                 │   │
│  │ }                                                   │   │
│  │                                                     │   │
│  │  适合:简单表单、文件上传、与第三方 DOM 库集成        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

9.2 常见陷阱:派生状态 ​

tsx
// ❌ 反模式:把 props 复制进 state
function Profile({ user }: { user: User }) {
  const [name, setName] = useState(user.name) // 仅仅在首次渲染时同步

  // 当 user prop 变化时,name state 不会自动更新
  // 两份数据不同步,这是 React 中最常见的 bug 之一
}

// ✅ 正确做法 1:直接使用 props(不要复制)
function Profile({ user }: { user: User }) {
  return <input value={user.name} onChange={...} />
}

// ✅ 正确做法 2:如果确实需要本地副本,明确语义并处理同步
function Profile({ user, onUpdate }: Props) {
  const [draft, setDraft] = useState(user.name)

  // 当外部 user 变化时,重置草稿
  useEffect(() => {
    setDraft(user.name)
  }, [user.id])  // 注意:用 id 而不是整个 user 对象
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第10部分:组合模式 ​

10.1 Compound Components(复合组件) ​

复合组件让多个组件隐式共享状态,对外暴露灵活的 API:

tsx
// 定义复合组件
type TabsContextType = {
  activeTab: string
  setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextType | null>(null)

function Tabs({ children, defaultTab }: {
  children: React.ReactNode
  defaultTab: string
}) {
  const [activeTab, setActiveTab] = useState(defaultTab)
  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabsContext.Provider>
  )
}

function TabList({ children }: { children: React.ReactNode }) {
  return <div role="tablist">{children}</div>
}

function Tab({ tab, children }: { tab: string; children: React.ReactNode }) {
  const ctx = useContext(TabsContext)!
  return (
    <button
      role="tab"
      aria-selected={ctx.activeTab === tab}
      onClick={() => ctx.setActiveTab(tab)}
    >
      {children}
    </button>
  )
}

function TabPanels({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

function TabPanel({ tab, children }: { tab: string; children: React.ReactNode }) {
  const ctx = useContext(TabsContext)!
  if (ctx.activeTab !== tab) return null
  return <div role="tabpanel">{children}</div>
}

// 使用 —— 声明式、可自由组合
<Tabs defaultTab="account">
  <TabList>
    <Tab tab="account">Account</Tab>
    <Tab tab="security">Security</Tab>
  </TabList>
  <TabPanels>
    <TabPanel tab="account"><AccountSettings /></TabPanel>
    <TabPanel tab="security"><SecuritySettings /></TabPanel>
  </TabPanels>
</Tabs>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

10.2 Slots 模式(Props 组合) ​

tsx
// 通过 props 暴露可替换的部分
type CardProps = {
  header?: React.ReactNode
  children: React.ReactNode
  footer?: React.ReactNode
}

function Card({ header, children, footer }: CardProps) {
  return (
    <div className="card">
      {header && <div className="card-header">{header}</div>}
      <div className="card-body">{children}</div>
      {footer && <div className="card-footer">{footer}</div>}
    </div>
  )
}

// 使用
<Card
  header={<><Icon name="user" /> Profile</>}
  footer={<Button>Save</Button>}
>
  <ProfileForm />
</Card>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

10.3 Render Props vs Hooks ​

┌─────────────────────────────────────────────────────────────┐
│              Render Props vs Hooks —— 如何选择               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Render Props(React 16 之前的主流复用模式)                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ function Mouse({ children }: {                      │   │
│  │   children: (pos: Point) => ReactNode               │   │
│  │ }) {                                                │   │
│  │   const [pos, setPos] = useState({ x:0, y:0 })     │   │
│  │   useEffect(() => {                                 │   │
│  │     const h = (e: MouseEvent) =>                    │   │
│  │       setPos({ x: e.clientX, y: e.clientY })        │   │
│  │     window.addEventListener('mousemove', h)         │   │
│  │     return () => window.removeEventListener(...)     │   │
│  │   }, [])                                            │   │
│  │   return <>{children(pos)}</>                       │   │
│  │ }                                                   │   │
│  │                                                     │   │
│  │ // 使用:                                           │   │
│  │ <Mouse>                                             │   │
│  │   {pos => <Cursor x={pos.x} y={pos.y} />}          │   │
│  │ </Mouse>                                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Hooks(现代推荐方式)                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ function useMouse(): Point {                        │   │
│  │   const [pos, setPos] = useState({ x:0, y:0 })     │   │
│  │   // ... 同样的 useEffect                          │   │
│  │   return pos                                        │   │
│  │ }                                                   │   │
│  │                                                     │   │
│  │ function Cursor() {                                 │   │
│  │   const pos = useMouse()                            │   │
│  │   return <div style={{ left: pos.x, top: pos.y }} />│   │
│  │ }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  选择原则:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ • 复用逻辑 → Hooks(几乎总是)                       │   │
│  │ • 复用 UI + 逻辑 → 组件 + Composition               │   │
│  │ • 需要渲染位置的灵活性 → Render Props(或 children)│   │
│  │ • 新项目 → 默认用 Hooks,只在有明确理由时用 RP     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

核心总结 ​

总结1:渲染三阶段是所有 React 行为的基础 ​

React 的每次更新都遵循 Render(计算新的 UI 描述)→ Reconcile(与当前 Fiber 树对比,标记差异)→ Commit(将差异应用到 DOM 并执行副作用)的三阶段流程。理解这个流程是理解"为什么组件被调用但 DOM 没更新""为什么 Effect 在 Commit 后执行""为什么 Render 必须纯净"的前提。

总结2:Fiber 让 React 可以"暂停" ​

Fiber 把协调工作拆分成以单个节点为粒度的小任务,通过 work loop 和 lane 优先级机制,让 React 可以在"更新一个包含 10000 行的列表"和"响应用户点击"之间做出取舍。这是 Concurrent Mode、Suspense、startTransition 等所有并发特性的底层基础。

总结3:Props 向下,事件向上,State 是快照 ​

React 的数据流有且只有一个方向。不要把 props 复制进 state(除非你非常清楚同步语义),不要试图在子组件中修改父组件的数据,不要在渲染中执行副作用。这些规则不是"最佳实践建议"——它们是 React 能正确运行的前提。

总结4:key 是身份,不是索引 ​

错误的 key 会导致 DOM 重建、输入状态错位、动画异常。key 应该来自业务数据的稳定标识符,而且只在当前兄弟列表中有意义。index 在静态列表中可以接受,但在任何可能重排序、插入、删除的列表中都会引发 bug。

总结5:先测量,再优化 ​

React.memo、useMemo、useCallback 有自己的比较开销和内存开销。在没有性能问题的地方使用它们是净负收益。使用 React DevTools Profiler 定位真正的瓶颈,通常更好的组件拆分和状态提升比 memo 更有效。


章节测试 ​

测试1:以下关于 React 渲染的描述哪个是正确的? ​

A. Render 阶段直接更新 DOM B. 组件函数被调用意味着对应的 DOM 一定被更新 C. Commit 阶段是同步且不可中断的 D. Reconcile 阶段执行 useEffect 回调

测试2:Fiber 架构的核心价值是什么? ​

A. 让 React 代码运行得更快 B. 让渲染过程可以被中断和恢复,主线程不被长时间占用 C. 替代虚拟 DOM D. 让组件可以使用 TypeScript

测试3:以下哪些操作在渲染期间是不允许的?(多选) ​

A. 读取 props B. 调用 setState 触发新渲染 C. 发起 fetch 请求 D. 计算派生值并返回 JSX E. 直接操作 DOM

测试4:以下关于 key 的说法哪个是错误的? ​

A. key 是兄弟节点之间的唯一身份标识 B. index 作为 key 在列表重排序时可能引发 bug C. key 会作为 props.key 传入组件 D. 不同父级下的 key 不需要全局唯一

测试5:以下关于并发特性的描述哪个是错误的? ​

A. startTransition 标记的更新可以被高优先级更新中断 B. useDeferredValue 适用于无法控制 setState 位置的场景 C. Strict Mode 的双重渲染只在开发环境执行 D. Suspense 只能用于代码分割,不能用于数据获取

测试6:关于 memo 的使用,以下哪个说法正确? ​

A. 所有组件都应该用 memo 包裹以获得最佳性能 B. memo 进行深层 props 比较 C. memo 的比较本身也有开销,应该先用 Profiler 测量再决定 D. memo 是 useMemo 的别名

测试7:受控组件和非受控组件的核心区别是什么? ​

A. 受控组件使用 TypeScript,非受控组件使用 JavaScript B. 受控组件由 React State 驱动值,非受控组件由 DOM 自己管理值 C. 受控组件更慢 D. 非受控组件不能使用 ref


参考答案 ​

测试1答案 ​

答案:C。Commit 阶段是同步且不可中断的(否则用户会看到半成品的 UI)。Render 阶段是纯粹的 JSX 计算,不操作 DOM。组件被调用(Render)不等于 DOM 一定更新——Reconcile 可能发现没有变化。useEffect 在 Commit 和浏览器绘制之后执行。

测试2答案 ​

答案:B。Fiber 架构的核心价值是将递归的、不可中断的协调过程改为可中断的链表遍历。它让 React 可以在处理大型更新时暂停、把控制权还给浏览器处理用户输入,然后恢复或丢弃未完成的工作。这是 Concurrent Mode 的基石。

测试3答案 ​

答案:B、C、E。渲染期间(Render Phase)调用 setState 可能触发无限循环,fetch 请求是副作用,直接操作 DOM 也是副作用。读取 props 和计算派生值是渲染期间的合法操作。所有副作用应该在 Event Handler、useEffect 或 useLayoutEffect 中执行。

测试4答案 ​

答案:C。key 不会作为 props.key 传入组件。如果需要业务 ID 在组件内部使用,需要单独通过 props 传递。key 是 React 内部使用的标识符,专用于兄弟节点之间的 reconciliation。

测试5答案 ​

答案:D。Suspense 既可以用于代码分割(React.lazy),也可以用于数据获取(需要支持 Suspense 的数据获取库如 TanStack Query 5+、Next.js App Router 等)。React 19 的 use() hook 进一步简化了 Suspense 数据获取的模式。

测试6答案 ​

答案:C。memo 进行的是浅层 props 比较(Object.is),本身也有开销。如果父组件每次传入新的对象/函数引用(即使内容相同),memo 也会失败。应该先用 Profiler 确认存在性能问题,再考虑 memo。通常"组件拆分"和"状态提升"比 memo 更根本地解决问题。

测试7答案 ​

答案:B。受控组件的值由 React State 驱动(value={state} + onChange={setState}),React 是单一数据源。非受控组件由 DOM 自身管理值,React 只在需要时通过 ref 读取。文件上传(<input type="file">)天然是非受控的,因为浏览器安全策略不允许 JS 设置文件路径。


相关笔记 ​

  • [[02-hooks-state-and-effects]] — Hooks 完整指南
  • [[03-routing-forms-and-component-architecture]] — 表单与组件架构实践
  • [[05-nextjs-app-router-and-rendering]] — Next.js App Router 与 RSC 渲染
  • [[00-overview]] — React 生态全景与学习路线

下一步学习 ​

  • [ ] 用 React DevTools Profiler 分析一个包含列表和搜索的项目,观察 Render/Commit 的耗时分布
  • [ ] 实现一个 Tabs 复合组件,体会 Context + Compound Components 的 API 设计
  • [ ] 在一个有输入框和大型列表的页面中应用 startTransition,对比优化前后的输入响应差异
  • [ ] 阅读 Hooks、状态与副作用 深入学习 useState、useEffect 和自定义 Hook

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. React 生态知识体系 / React Ecosystem Knowledge System
下一篇3. Hooks 深度剖析:状态、副作用与并发模式 / Hooks Deep Dive: State, Effects, and Concurrency

持续记录,持续成长

Copyright © Tidenflow