专题知识学习:React 知识体系

第一章:React 基础与核心概念

1.1 React 简介与设计哲学(组件化、声明式 UI)

基础概念概述

React 是什么?

Facebook 推出的用于构建用户界面的 JavaScript 库,核心解决 UI 组件化开发问题。

两大设计哲学

  • 组件化 (Component-Based):将 UI 拆分为独立、可复用的代码单元(组件),每个组件管理自身状态与逻辑。示例:<Button />, <Card />
  • 声明式 UI (Declarative):开发者只需描述“UI 应该是什么样子”(What),而非“如何更新到该状态”(How)。对比命令式:直接操作 DOM vs. 返回 JSX 描述结构

深入原理与面试考点

组件化背后的核心思想

特性 说明 面试举例
封装性 组件内部状态/逻辑对外隐藏,仅通过 Props 通信 “为何 React 推荐单向数据流?”
组合性 通过嵌套组件构建复杂 UI(如 <Form><Input/><Button/></Form> “如何设计可复用的高阶组件?”
复用性 同一组件在不同场景重复使用 “受控组件与非受控组件适用场景差异?”

声明式 UI 的底层实现

// 声明式写法(What)
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

// 对应的命令式逻辑(How)
const button = document.querySelector('button');
let count = 0;
button.addEventListener('click', () => {
  count++;
  button.textContent = count; // 需手动更新 DOM
});

关键优势:

  • 避免直接操作 DOM → 减少代码复杂度与潜在 Bug
  • 与状态绑定 → UI 自动随状态更新(通过 Virtual DOM 协调)

虚拟 DOM (Virtual DOM) 的角色

graph LR
A[组件状态变化] --> B{生成新 Virtual DOM}
B --> C[对比新旧 Virtual DOM]
C --> D[计算最小 DOM 变更]
D --> E[批量更新真实 DOM]

设计哲学关联:声明式 UI 依赖 Virtual DOM 实现高效的 “状态到 UI 的映射”,开发者无需关注更新过程。

面试话术技巧

Q:为什么 React 采用声明式而非命令式?

声明式让代码更专注于业务逻辑的描述(What),将底层 DOM 操作交给 React 的 Virtual DOM 协调机制处理。这带来三方面优势:

  1. 可维护性:状态变化自动触发 UI 更新,避免手动 DOM 操作导致的逻辑分散
  2. 性能优化:React 通过 Diff 算法批量 DOM 更新,减少重绘开销
  3. 开发体验:更接近自然思维模式的 UI 构建方式,提升开发效率”

延伸考点

  • JSX 的本质:声明式 UI 的语法糖(编译为 React.createElement())
  • 组件化与设计模式:复合组件 (Compound Components)、容器/展示组件等

1.2 JSX 语法与渲染逻辑(表达式、条件渲染、列表渲染)

基础概念简述

JSX 是什么?

JavaScript 语法扩展,允许在 JavaScript 中编写类似 HTML 的结构(语法糖)

三大核心功能

  • 表达式嵌入:{ } 包裹任意 JavaScript 表达式
  • 条件渲染:三元运算符 ? : 或 && 短路运算
  • 列表渲染:map() + key 属性生成元素列表

核心机制与深度原理

JSX 编译原理(Babel 转换过程)

// 原始 JSX
const element = <div className="container">Hello {name}</div>;

// Babel 编译后(React 17+ 无需显式引入 React)
import { jsx as _jsx } from "react/jsx-runtime";
const element = _jsx("div", {
  className: "container",
  children: ["Hello ", name]
});

关键点:

  • JSX 本质是 React.createElement(type, props, children) 的语法糖
  • 每个 JSX 元素会被编译为 虚拟 DOM 对象(包含 type, props, key 等属性)

表达式嵌入的运行时机制

function Welcome(props) {
  // 表达式执行发生在组件 render 阶段
  return <h1>当前用户: {props.user ?? '游客'}</h1>;
}

注意事项:

  • 表达式内不能使用语句(如 if/for)
  • 表达式结果类型自动转换:
    • undefined/null/true/false → 不渲染
    • 数组 → 展开渲染(需包含有效元素)
    • 对象 → 抛出错误(除特殊对象如 ReactElement)

条件渲染的底层实现策略

// 方案1:三元表达式(适合简单分支)
{ isLoggedIn ? <LogoutButton /> : <LoginButton /> }

// 方案2:&& 短路(适合单分支)
{ hasUnread && <Badge count={unreadCount} /> }

// 方案3:立即执行函数(复杂逻辑 - 不推荐)
{ (() => {
    if (conditionA) return <ComponentA />;
    if (conditionB) return <ComponentB />;
    return <Fallback />;
  })() 
}

性能优化:条件变化时,React 通过 Diff 算法 复用相同子组件(按组件位置比对)

列表渲染的 key 机制

const todoItems = todos.map(todo => (
  <li key={todo.id}>  // ⭐ 关键点:key 必须是稳定唯一标识
    {todo.text}
  </li>
));

无 key 的隐患:

  • 列表顺序变化时引发组件状态错乱(如输入框内容错位)
  • 性能下降:全量比对子元素(时间复杂度 O(n²))

高频面试题与破解技巧

Q:为什么 JSX 中 {boolean && <Component/>} 可能渲染 0?

true && jsx 返回 jsx,但 false && jsx 返回 false → React 将 false 渲染为空白占位符(类似 null),导致 DOM 中出现空文本节点。

解决方案:

// 转换为 null 避免空节点
{ shouldRender && <Component /> || null }

Q:列表渲染为什么不能用 index 作为 key?

当列表发生增删或排序时:

  • index 变化导致组件被意外销毁重建(状态丢失)
  • Diff 算法无法准确定位节点变化
  • 正确做法:使用数据唯一 ID(如 todo.id)

Q:JSX 如何防止 XSS 攻击?

JSX 在执行表达式插入时自动进行转义处理:

const title = '<script>alert(1)</script>';
<h1>{title}</h1> 
// 渲染为:&lt;script&gt;alert(1)&lt;/script&gt;

例外:dangerouslySetInnerHTML 需手动防范

1.3 组件基础(函数组件 vs. 类组件)

基础概念概述

特性 函数组件 类组件
定义方式 JavaScript 函数 ES6 class 继承 React.Component
状态管理 useState Hook (React 16.8+) this.state + setState
生命周期 useEffect Hook 生命周期方法(componentDidMount等)
代码量 更简洁 相对冗长
推荐场景 现代 React 开发首选 遗留项目或需要 Error Boundaries 时

核心机制与差异深度解析

渲染逻辑本质区别

// 函数组件:纯函数执行
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 类组件:实例化后调用 render 方法
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

关键差异:

  • 函数组件:直接返回 JSX,每次渲染都是独立函数调用
  • 类组件:通过 实例化 → 调用 render() 返回 JSX,实例在更新间保持

状态管理的底层实现

// 函数组件:闭包存储状态(Hook 链表结构)
function Counter() {
  const [count, setCount] = useState(0); 
  // React 内部通过顺序索引关联状态
}

// 类组件:实例属性存储状态
class Counter extends React.Component {
  state = { count: 0 };
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  }
}

状态更新差异:

特性 函数组件 类组件
更新机制 闭包捕获当次渲染状态 始终读取最新 this.state
批处理 自动批处理(React 18+) 需 unstable_batchedUpdates
异步性 状态更新队列 setState 合并更新

生命周期映射关系

graph TD
  A[类组件生命周期] --> B[componentDidMount]
  A --> C[componentDidUpdate]
  A --> D[componentWillUnmount]
  
  E[函数组件等效实现] --> F["useEffect(() => {}, [])"]
  E --> G["useEffect(() => {}, [deps])"]
  E --> H["useEffect(() => { return () => {} }, [])"]
  
  B -->|挂载后执行| F
  C -->|依赖更新后执行| G
  D -->|卸载前清理| H

关键差异点:

  • 类组件:生命周期方法分散定义,逻辑易割裂,componentDidMount/Update 在 浏览器绘制后 执行
  • 函数组件:useEffect 集中处理副作用,通过依赖数组控制执行时机,useEffect 在 浏览器绘制前 异步执行(使用 useLayoutEffect 可模拟同步行为)

高频面试题与破解技巧

Q:为什么函数组件取代了类组件成为主流?

  • 代码简洁性:减少约 30% 代码量(无 class/this/constructor)
  • 逻辑复用优势:自定义 Hook 比 HOC/Render Props 更直观
  • 性能优化:避免类组件实例化开销,Hook 依赖数组精准控制更新
  • 未来兼容:React 新特性(Concurrent Features)优先支持函数组件

Q:函数组件如何实现类似 forceUpdate 的功能?

const [_, forceUpdate] = useReducer(x => x + 1, 0);

// 调用 forceUpdate() 触发重渲染

原理:通过修改无关状态强制触发更新

Q:类组件中 setState 是同步还是异步?

  • React 17 及之前:合成事件/生命周期中异步,setTimeout 中同步
  • React 18+:默认全部异步(自动批处理)
    // 获取更新后状态的正确方式
    this.setState({ count: 42 }, () => {
      console.log('更新后:', this.state.count); // 回调函数保证
    });

1.4 Props 与 State(数据流、单向数据绑定)

基础概念对比表

特性 Props State
定义 组件外部传入的数据 组件内部管理的状态
可变性 只读(Immutable) 可修改(通过 setState/useState)
数据源 父组件传递 组件自身创建
更新触发 父组件重渲染 调用状态更新函数
通信方向 父 → 子 组件内部

核心机制深度解析

单向数据流(Unidirectional Data Flow)

graph LR
A[父组件] -- Props --> B[子组件1]
A -- Props --> C[子组件2]
B -- Props --> D[孙子组件]
C -- Props --> E[孙子组件]

核心规则:

  • 数据只能从父组件流向子组件,禁止反向流动
  • 子组件修改父组件数据需通过回调函数实现:
    // 父组件
    function Parent() {
      const [value, setValue] = useState('');
      return <Child value={value} onChange={setValue} />;
    }
    
    // 子组件
    function Child({ value, onChange }) {
      return <input value={value} onChange={e => onChange(e.target.value)} />;
    }

Props 的不可变性(Immutability)

底层原理:

  • React 使用 Object.is 算法进行 props 浅比较(shallow compare)
  • 若检测到 props 引用变化,则触发子组件重渲染

开发约束:

// 禁止直接修改 props
function UserCard({ user }) {
  user.name = "Hacked";  // 违反不可变性原则
  return <div>{user.name}</div>;
}

// 正确做法:使用派生数据
function UserCard({ user }) {
  const displayName = user.name.toUpperCase();  // 创建新值
  return <div>{displayName}</div>;
}

State 更新机制

(1) 类组件

class Counter extends React.Component {
  state = { count: 0 };
  
  increment = () => {
    // 批量更新:多次 setState 合并为一次更新
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // 结果:count 只 +1(使用对象形式)
    
    // 函数形式解决连续更新
    this.setState(prev => ({ count: prev.count + 1 }));
    this.setState(prev => ({ count: prev.count + 1 }));
    // 结果:count +2
  }
}

(2) 函数组件

function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);  // 闭包捕获当前 count 值
    setCount(count + 1);  // 仍基于初始值计算
    
    // 函数形式获取最新值
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // 正确 +2
  };
}

高频面试题与破解方案

Q:为什么React强调Props不可变?

  • 可预测性:确保组件行为只取决于输入的props,便于调试
  • 性能优化:浅比较依赖引用不变性(若直接修改,浅比较无法检测变化)
  • 并发安全:避免数据在渲染过程中被意外修改

Q:如何实现子组件向父组件通信?

设计模式:

// 父组件
function Parent() {
  const [data, setData] = useState(null);
  
  // 回调函数作为prop传递
  const handleChildSubmit = (childData) => {
    setData(childData);
  };

  return <Child onSubmit={handleChildSubmit} />;
}

// 子组件
function Child({ onSubmit }) {
  const [input, setInput] = useState('');
  
  return (
    <>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button onClick={() => onSubmit(input)}>提交</button>
    </>
  );
}

Q:什么时候该用Props vs State?

决策过程:

graph TD
A[数据来源] -->|父组件传递| B[用Props]
A -->|组件自身管理| C[用State]
D[是否随时间/交互变化] -->|否| E[用Props]
D -->|是| F[用State]

高级应用场景

状态提升(Lifting State Up)

场景:多个组件需要共享同一状态

function Parent() {
  // 状态提升到共同父组件
  const [theme, setTheme] = useState('light');
  
  return (
    <>
      <Toolbar theme={theme} onThemeChange={setTheme} />
      <ContentArea theme={theme} />
    </>
  );
}

// 子组件通过props获取/更新状态

性能优化陷阱

Props 穿透问题

// ❌ 不必要重渲染:直接传递对象
<Child user={user} />

// ✅ 解决方案:扁平化props或React.memo
<Child name={user.name} age={user.age} />

状态位置错误导致的渲染风暴

// ❌ 状态放在过高层级
function App() {
  const [counter, setCounter] = useState(0);  // 任何更新触发全应用重渲染
  return <Page />;
}

// ✅ 状态下沉到最小范围
function CounterButton() {
  const [count, setCount] = useState(0);  // 影响范围局部化
  return <button onClick={() => setCount(c+1)}>{count}</button>;
}

1.5 事件处理(合成事件、this 绑定问题)

基础概念概述

React 事件处理特点:

  • 驼峰命名:onClick 而非 onclick
  • JSX 中传入函数引用而非字符串
  • 默认阻止默认行为需显式调用 e.preventDefault()

两大核心问题:

  • 合成事件(SyntheticEvent)机制
  • 类组件中的 this 绑定问题

合成事件(SyntheticEvent)深度解析

设计目的与原理

graph LR
A[浏览器原生事件] --> B(React事件系统)
B --> C[创建合成事件对象]
C --> D[事件委托到根容器]
D --> E[触发对应组件处理函数]
E --> F[事件池回收]

核心特性:

  • 跨浏览器兼容:统一不同浏览器事件接口
  • 性能优化:事件委托 + 事件池复用(React 17 前)
  • 冒泡机制:基于虚拟DOM树而非实际DOM树

事件池机制(React 17 前)

function handleClick(e) {
  console.log(e.target); // 正常访问
  
  setTimeout(() => {
    console.log(e.target); // ❌ React 17前:null(事件对象已被回收)
  }, 0);
  
  // ✅ 解决方案:e.persist() 保留事件对象
}

React 17+ 改进:移除了事件池,无需 e.persist()

合成事件与原生事件差异

特性 合成事件 原生事件
事件注册 onClick addEventListener
事件对象 SyntheticEvent 原生 Event 对象
事件传播 虚拟DOM树冒泡 实际DOM树冒泡
阻止冒泡 e.stopPropagation() 同名API
事件委托 自动委托到根容器 需手动委托

this 绑定问题详解(类组件)

问题根源

class Button extends React.Component {
  handleClick() {
    // ❌ 此时 this 为 undefined(严格模式)
    console.log(this.props); 
  }
  
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

原因:JavaScript 函数中的 this 由调用方式决定,非类实例本身

四种解决方案对比

方案 代码示例 优点 缺点
构造函数绑定 this.handleClick = this.handleClick.bind(this) 标准做法 代码冗余
箭头函数(类属性) handleClick = () => {…} 简洁,推荐方案 实验性语法(需Babel)
内联箭头函数 onClick={() => this.handleClick()} 简单直接 每次渲染创建新函数
bind 内联 onClick={this.handleClick.bind(this)} 无额外代码 同内联箭头函数

性能优化关键点

// ❌ 避免内联绑定(每次渲染创建新函数)
<button onClick={() => this.handleClick(id)}>点击</button>

// ✅ 推荐:构造函数预绑定
constructor() {
  this.handleClick = this.handleClick.bind(this);
}

// ✅ 或使用类属性箭头函数
handleClick = (id) => { ... }

高频面试题与解决方案

Q:为什么React不直接使用原生事件?

  • 跨浏览器兼容:统一事件处理逻辑
  • 性能优化:事件委托减少内存占用
  • 事件池机制:复用事件对象(React 17前)
  • 框架扩展性:为异步渲染等特性铺平道路

Q:如何阻止合成事件的默认行为?

function Form() {
  const handleSubmit = (e) => {
    e.preventDefault(); // ✅ 显式阻止表单提交
    // 处理逻辑...
  };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Q:合成事件和原生事件混用时要注意什么?

componentDidMount() {
  document.addEventListener('click', this.handleDocumentClick);
}

handleDocumentClick = () => {
  // 原生事件
};

handleButtonClick = (e) => {
  // 合成事件
  e.stopPropagation(); // ❌ 无法阻止原生事件冒泡
};

// ✅ 正确方案:在原生事件中使用 nativeEvent
handleButtonClick = (e) => {
  e.nativeEvent.stopImmediatePropagation();
};

React 17+ 事件系统升级

  • 委托目标变更:从 document 到 ReactDOM.render 的根容器
    // React 16:委托到 document
    // React 17+:委托到 root DOM 容器
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
  • 移除事件池:无需 e.persist() 可异步访问事件对象
  • 更接近原生:e.stopPropagation() 真正阻止原生事件传播

第二章:React 核心机制

2.1 虚拟 DOM 与 Diff 算法(Reconciliation)

基础概念概述

  • 虚拟 DOM (Virtual DOM):
    • JavaScript 对象表示的 DOM 副本
    • 轻量化的内存数据结构
    • 与实际 DOM 结构一一对应
  • Diff 算法 (Reconciliation):
    • React 比较新旧虚拟 DOM 差异的算法
    • 时间复杂度优化到 O(n)
    • 生成最小化 DOM 操作指令

虚拟 DOM 核心原理

虚拟 DOM 结构示例

// 实际 JSX
<div className="container">
  <h1 key="title">Hello React</h1>
  <List items={items} />
</div>

// 对应的虚拟 DOM 对象
{
  type: 'div',
  props: { className: 'container' },
  children: [
    {
      type: 'h1',
      props: { key: 'title' },
      children: 'Hello React'
    },
    {
      type: List,  // 组件类型
      props: { items },
      // ... 内部有独立虚拟 DOM 树
    }
  ]
}

虚拟 DOM 工作流程

graph LR
A[组件状态变化] --> B[创建新虚拟 DOM 树]
B --> C[Diff 算法比较新旧树]
C --> D[生成 DOM 补丁包]
D --> E[批量更新真实 DOM]

虚拟 DOM 性能优势

操作 直接操作 DOM 虚拟 DOM 方案
更新 10 节点 10 次 DOM 操作 1 次 Diff + 1 次更新
跨平台能力 依赖浏览器 API 抽象渲染层
复杂更新 手动优化难度大 自动批量处理

Diff 算法深度解析(O(n) 优化策略)

算法三大设计原则

(1) 同级比较:只比较同一层级的节点,不跨级比较

graph TD
A[旧树] --> B[节点A]
A --> C[节点B]
D[新树] --> E[节点A']
D --> F[节点B']
B -->|比较| E
C -->|比较| F

(2) 类型差异直接替换:节点类型不同时直接卸载整棵子树

// 旧:<div><ComponentA /></div>
// 新:<span><ComponentB /></span>
// React 操作:卸载整个 div 及子组件,创建 span 和 ComponentB

(3) Key 属性优化列表:列表项使用 key 标识身份,减少节点移动开销

列表 Diff 的 key 机制

// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>

// 新列表(删除B,新增D)
<ul>
  <li key="a">A</li>
  <li key="c">C</li>
  <li key="d">D</li>
</ul>

Diff 过程:

  • 通过 key 匹配新旧节点:a→a, c→c
  • 删除 key=”b” 的节点
  • 新增 key=”d” 的节点
  • 复用 key=”a” 和 “c” 的节点

节点复用策略

场景 操作 性能影响
相同类型 DOM 元素 更新属性 高效
相同类型组件元素 触发更新(不卸载) 保留组件状态
不同类型元素 卸载整棵子树 成本较高

React Fiber 架构与可中断渲染

传统 Diff 算法局限

  • 递归遍历:无法中断长时间任务
  • 阻塞主线程:导致动画卡顿

Fiber 架构核心改进

graph LR
A[同步递归] --> B[可中断异步任务]
C[虚拟DOM树] --> D[Fiber链表结构]

Fiber 节点结构:

{
  type: 'div',          // 节点类型
  key: null,            // 唯一标识
  return: parentFiber,  // 父节点
  child: firstChild,    // 第一个子节点
  sibling: nextSibling, // 兄弟节点
  alternate: oldFiber,  // 指向旧树对应节点
  effectTag: 'UPDATE',  // 需要执行的副作用
  // ... 其他元数据
}

双缓存机制(Current 树与 WorkInProgress 树)

graph LR
A[当前显示] -->|Current 树| B[用户界面]
C[正在构建] -->|WorkInProgress 树| D[内存中]
B -->|渲染完成| C
  • 无闪烁更新:直接切换树引用
  • 安全回滚:中断时丢弃未完成树

高频面试题与破解方案

Q:为什么列表渲染需要 key?key 可以随机数吗?

  • key 帮助 React 识别元素身份,减少不必要的销毁/重建
  • 禁止使用随机数:每次渲染 key 变化导致全量重建
  • 推荐方案:稳定 ID(如数据 id)或索引(仅静态列表)

Q:虚拟 DOM 一定比直接操作 DOM 快吗?

辩证回答:

  • 初始渲染:虚拟 DOM 需要额外创建对象,稍慢
  • 复杂更新:虚拟 DOM 通过 Diff 减少操作次数,更快
  • 综合优势:
    • 跨平台能力(React Native)
    • 声明式编程体验
    • 自动批处理优化

Q:React 如何实现 O(n) 复杂度的 Diff?

算法原理:

  • 仅同层比较(深度优先遍历)
  • 类型不同时跳过子树比较
  • 列表使用 key 匹配减少移动开销

性能优化技巧

避免不必要节点变动

// ❌ 每次渲染创建新对象(导致子组件重渲染)
<Child style={{ color: 'red' }} />

// ✅ 提取静态对象
const staticStyle = { color: 'red' };
<Child style={staticStyle} />

列表渲染优化

// ❌ 索引作为 key(列表变化时状态错乱)
{todos.map((todo, index) => 
  <TodoItem key={index} todo={todo} />
)}

// ✅ 稳定 ID 作为 key
{todos.map(todo => 
  <TodoItem key={todo.id} todo={todo} />
)}

组件树结构优化

// ❌ 深层嵌套导致 Diff 范围大
<App>
  <Header>
    <UserPanel>  // 状态变化影响整个 Header
      <Avatar />
    </UserPanel>
  </Header>
</App>

// ✅ 状态下沉 + 组件提权
<App>
  <Header />
  <UserPanel>   // 独立更新
    <Avatar />
  </UserPanel>
</App>

React 18 并发渲染优化

可中断渲染

graph LR
A[高优先级更新] --> B[中断当前渲染]
B --> C[执行紧急任务]
C --> D[恢复渲染]

自动批处理

// React 17:仅事件处理函数内批处理
setTimeout(() => {
  setCount(1);
  setFlag(true); // 两次独立更新
}, 100);

// React 18:所有更新自动批处理
setTimeout(() => {
  setCount(1);   // 合并为一次更新
  setFlag(true);
}, 100);

2.2 生命周期(类组件生命周期、useEffect 替代方案)

类组件生命周期全景图

graph TD
  A[挂载阶段] --> B[constructor]
  B --> C[static getDerivedStateFromProps]
  C --> D[render]
  D --> E[componentDidMount]
  
  F[更新阶段] --> G[static getDerivedStateFromProps]
  G --> H[shouldComponentUpdate]
  H --> I[render]
  I --> J[getSnapshotBeforeUpdate]
  J --> K[componentDidUpdate]
  
  L[卸载阶段] --> M[componentWillUnmount]
  
  N[错误处理] --> O[static getDerivedStateFromError]
  O --> P[componentDidCatch]

各阶段核心方法:

阶段 方法 执行时机 常见用途
挂载 constructor 组件初始化 初始化 state,绑定 this
挂载 render 创建虚拟 DOM 返回 JSX 内容
挂载 componentDidMount DOM 挂载完成 网络请求、订阅事件
更新 shouldComponentUpdate 渲染前拦截 性能优化(返回 false 阻止更新)
更新 getSnapshotBeforeUpdate DOM 更新前 获取滚动位置等 DOM 信息
更新 componentDidUpdate DOM 更新完成 基于新 DOM 操作
卸载 componentWillUnmount 组件卸载前 清理定时器、取消订阅
错误 componentDidCatch 子组件抛出错误 错误日志、显示降级 UI

函数组件 useEffect 替代方案

生命周期映射关系:

graph LR
  A[componentDidMount] --> B["useEffect(() => {}, [])"]
  C[componentDidUpdate] --> D["useEffect(() => {}, [deps])"]
  E[componentWillUnmount] --> F["useEffect(() => { return cleanup }, [])"]
  G[shouldComponentUpdate] --> H[React.memo 或 useMemo]

核心机制深度解析

useEffect 执行时机

方法 执行阶段 是否阻塞渲染 访问 DOM
componentDidMount 浏览器绘制后 可访问
componentDidUpdate 浏览器绘制后 可访问
useEffect 浏览器绘制前 否(异步) 可访问(但需注意时机)
useLayoutEffect DOM 变更后,绘制前 是(同步) 可访问

依赖数组精妙之处

// ✅ 安全:包含所有依赖
useEffect(() => {
  const total = price * quantity;
  setTotal(total);
}, [price, quantity]); 

// ❌ 危险:缺少依赖(使用过期状态)
useEffect(() => {
  const total = price * quantity;
  setTotal(total);
}, [price]); // 缺少 quantity

// ✅ 解决方案:函数式更新
useEffect(() => {
  setTotal(prev => price * quantity);
}, [price]); // 不再依赖 quantity

高频面试题与破解方案

Q:为什么 useEffect 可能执行两次?

React 18+ 严格模式行为:

  • 开发环境下故意双调用:
    <React.StrictMode>
      <App /> {/* useEffect 挂载/卸载各执行两次 */}
    </React.StrictMode>
  • 目的:检测不纯渲染(例如未正确实现清理)
  • 解决方案:确保清理函数完全复原初始状态

Q:如何替代 shouldComponentUpdate?

// 方案1:React.memo 浅比较
const MyComponent = React.memo(({ data }) => { ... });

// 方案2:自定义比较
const MyComponent = React.memo(
  ({ data }) => { ... },
  (prevProps, nextProps) => {
    return prevProps.id === nextProps.id; // 自定义比较逻辑
  }
);

// 方案3:useMemo 控制子组件
function Parent() {
  const memoizedChild = useMemo(
    () => <Child data={complexData} />,
    [complexData] // 仅 complexData 变化时重建
  );
  return <div>{memoizedChild}</div>;
}

Q:getSnapshotBeforeUpdate 如何替代?

// 类组件
class ScrollList extends React.Component {
  getSnapshotBeforeUpdate() {
    return this.list.scrollHeight;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    this.list.scrollTop += this.list.scrollHeight - snapshot;
  }
}

// 函数组件:useLayoutEffect + useRef
function ScrollList() {
  const listRef = useRef();
  const lastScrollHeight = useRef();
  
  useLayoutEffect(() => {
    // DOM 更新后,绘制前执行(类似 getSnapshotBeforeUpdate)
    lastScrollHeight.current = listRef.current.scrollHeight;
    
    return () => {
      // 清理阶段相当于 getSnapshotBeforeUpdate
      const snapshot = listRef.current.scrollHeight;
      listRef.current.scrollTop += listRef.current.scrollHeight - lastScrollHeight.current;
    };
  }, [items]);
}

错误边界(Error Boundaries)特殊说明

类组件专属能力

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }
  
  render() {
    return this.state.hasError
      ? <FallbackComponent />
      : this.props.children;
  }
}

函数组件解决方案

// 使用第三方库 react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <UnstableComponent />
    </ErrorBoundary>
  );
}

2.3 受控组件 vs. 非受控组件(表单处理)

核心概念对比

特性 受控组件 非受控组件
数据管理 React state 完全控制 DOM 节点自身管理
值获取 value 属性 ref 访问 DOM 节点
值更新 onChange 事件 + setState 用户直接输入
初始化 value 属性 defaultValue/defaultChecked
表单提交 从 state 获取 从 ref 获取
实时验证 容易实现 需要手动监听事件
适用场景 复杂表单、即时验证 简单表单、文件上传

受控组件深度解析

实现原理

function ControlledForm() {
  const [value, setValue] = useState('');
  
  const handleChange = (e) => {
    setValue(e.target.value); // 完全控制输入值
  };
  
  return (
    <input 
      type="text"
      value={value}       // 值绑定 state
      onChange={handleChange} // 更新 state
    />
  );
}

多字段优化模式

function MultiFieldForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: ''
  });

  // 通用变更处理器
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  return (
    <>
      <input name="name" value={formData.name} onChange={handleChange} />
      <input name="email" value={formData.email} onChange={handleChange} />
    </>
  );
}

即时验证示例

function EmailInput() {
  const [email, setEmail] = useState('');
  const [isValid, setIsValid] = useState(false);
  
  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    setIsValid(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)); // 实时验证
  };
  
  return (
    <div>
      <input type="email" value={email} onChange={handleChange} />
      {!isValid && <span>邮箱格式错误</span>}
    </div>
  );
}

非受控组件深度解析

实现原理

function UncontrolledForm() {
  const inputRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(inputRef.current.value); // 提交时获取值
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text"
        ref={inputRef}          // 绑定 ref
        defaultValue="初始值"   // 仅初始值
      />
      <button>提交</button>
    </form>
  );
}

文件上传场景(必须使用非受控)

function FileUpload() {
  const fileRef = useRef(null);
  
  const handleSubmit = () => {
    const file = fileRef.current.files[0];
    // 处理文件...
  };
  
  return (
    <div>
      <input type="file" ref={fileRef} /> {/* 无法受控 */}
      <button onClick={handleSubmit}>上传</button>
    </div>
  );
}

第三方库集成

function RichTextEditor() {
  const editorRef = useRef(null);
  
  useEffect(() => {
    // 初始化第三方编辑器
    const editor = new ThirdPartyEditor(editorRef.current);
    return () => editor.destroy();
  }, []);
  
  return <div ref={editorRef} />; // 非受控管理
}

高频面试题与破解方案

Q:为什么文件输入必须用非受控组件?

  • 安全限制:浏览器禁止 JavaScript 设置文件输入值(<input type="file">
  • 只读属性:文件路径由用户选择,程序无法控制
  • 数据获取:只能通过 ref.current.files 获取文件对象

Q:受控组件相比非受控有何优势?

  • 即时验证:每次输入都可触发验证逻辑
  • 条件渲染:基于输入值动态控制其他 UI
  • 状态追溯:完整的状态历史记录
  • 表单重置:轻松实现 reset 功能(setState(‘’))

Q:如何实现表单重置功能?

受控组件方案:

function ResettableForm() {
  const [form, setForm] = useState({ name: '', email: '' });
  
  const handleReset = () => {
    setForm({ name: '', email: '' }); // 重置 state
  };
  
  return (
    <form>
      <input value={form.name} onChange={/*...*/} />
      <button type="button" onClick={handleReset}>重置</button>
    </form>
  );
}

非受控组件:

function ResettableForm() {
  const formRef = useRef();
  
  const handleReset = () => {
    formRef.current.reset(); // 调用原生 DOM reset
  };
  
  return (
    <form ref={formRef}>
      <input defaultValue="" />
      <button type="button" onClick={handleReset}>重置</button>
    </form>
  );
}

2.4 组件通信(父子通信、Context API、事件总线)

组件通信方式全景图

graph TD
  A[组件通信] --> B[父子组件]
  A --> C[兄弟组件]
  A --> D[祖孙组件]
  A --> E[任意组件]
  
  B --> B1[Props/回调函数]
  C --> C1[状态提升]
  C --> C2[通过共同父组件]
  D --> D1[Context API]
  D --> D2[逐层传递Props]
  E --> E1[事件总线]
  E --> E2[状态管理库]

父子组件通信(Props + 回调)

基础模式

// 父组件
function Parent() {
  const [data, setData] = useState('');
  
  const handleChildData = (childData) => {
    setData(childData);
  };

  return <Child onDataSend={handleChildData} />;
}

// 子组件
function Child({ onDataSend }) {
  return <button onClick={() => onDataSend('Hello')}>发送数据</button>;
}

多层透传问题

// 祖孙组件通信(不推荐)
function Grandparent() {
  return <Parent data={data} />;
}

function Parent({ data }) {
  return <Child data={data} />; // 中间层被迫传递
}

function Child({ data }) {
  return <div>{data}</div>;
}

痛点:Props drilling(属性钻探)导致代码冗余

Context API 深度解析

核心三要素

// 1. 创建Context
const ThemeContext = React.createContext('light'); // 默认值

// 2. Provider 提供数据
function App() {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. Consumer 消费数据(两种方式)
// 方式1:useContext Hook
function Button() {
  const { theme } = useContext(ThemeContext);
  return <button className={theme}>按钮</button>;
}

// 方式2:Context.Consumer
function Button() {
  return (
    <ThemeContext.Consumer>
      {({ theme }) => <button className={theme}>按钮</button>}
    </ThemeContext.Consumer>
  );
}

性能优化策略

// ❌ 错误写法:直接传递对象
<MyContext.Provider value={{ theme, setTheme }}>
  ...
</MyContext.Provider>

// ✅ 正确方案:useMemo 缓存值
const contextValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
  <MyContext.Provider value={contextValue}>
    ...
  </MyContext.Provider>
);

多层Context嵌套

<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <LayoutContext.Provider value={layout}>
      <AppContent />
    </LayoutContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

// 消费时自动匹配最近Provider
function Header() {
  const user = useContext(UserContext);       // 获取user
  const theme = useContext(ThemeContext);     // 获取theme
  const layout = useContext(LayoutContext);   // 获取layout
}

事件总线(Event Bus)模式

实现原理

// eventBus.js
const events = {};

export default {
  $on(eventName, fn) {
    events[eventName] = events[eventName] || [];
    events[eventName].push(fn);
  },
  
  $off(eventName, fn) {
    if (events[eventName]) {
      events[eventName] = events[eventName].filter(f => f !== fn);
    }
  },
  
  $emit(eventName, data) {
    if (events[eventName]) {
      events[eventName].forEach(fn => fn(data));
    }
  }
};

React 组件中使用

// 组件A(发布事件)
import eventBus from './eventBus';

function ComponentA() {
  const handleClick = () => {
    eventBus.$emit('message', '数据内容');
  };
  
  return <button onClick={handleClick}>发送</button>;
}

// 组件B(订阅事件)
function ComponentB() {
  const [msg, setMsg] = useState('');
  
  useEffect(() => {
    const handler = (data) => setMsg(data);
    eventBus.$on('message', handler);
    
    return () => eventBus.$off('message', handler); // 清理
  }, []);
  
  return <div>{msg}</div>;
}

适用场景与风险

优点 缺点 适用场景
跨任意组件通信 破坏组件独立性 微前端架构通信
解耦性强 难以追踪数据流 非父子组件简单交互
实现简单 可能内存泄漏(未及时取消订阅) 第三方库事件通知

高级通信模式

状态提升 + Context 组合

// 创建共享状态上下文
const SharedStateContext = React.createContext();

function App() {
  const [state, setState] = useState(initialState);
  
  return (
    <SharedStateContext.Provider value={{ state, setState }}>
      <ComponentA />
      <ComponentB />
    </SharedStateContext.Provider>
  );
}

// 任意子组件消费
function ComponentA() {
  const { state, setState } = useContext(SharedStateContext);
  // ...
}

Render Props 模式

// 数据提供者
class DataProvider extends React.Component {
  state = { data: null };
  
  componentDidMount() {
    fetchData().then(data => this.setState({ data }));
  }
  
  render() {
    return this.props.children(this.state.data); // 关键
  }
}

// 消费组件
function Consumer() {
  return (
    <DataProvider>
      {data => data ? <ShowData data={data} /> : <Loading />}
    </DataProvider>
  );
}

观察者模式(Pub/Sub)

// 使用第三方库 (rxjs)
import { Subject } from 'rxjs';

const message$ = new Subject();

// 发布
function publishMessage(text) {
  message$.next(text);
}

// 订阅
message$.subscribe(text => {
  console.log('收到消息:', text);
});

高频面试题与破解方案

Q:Context 与 Redux 如何选择?

graph TD
A[需要全局状态?] -->|是| B[状态更新频率高?]
A -->|否| C[用Props/状态提升]
B -->|是| D[用Redux/Zustand]
B -->|否| E[用Context API]
  • 关键区别:
    • Context:轻量级,适合低频更新(主题/用户信息)
    • Redux:重量级,适合高频更新+时间旅行调试

Q:如何避免 Context 性能问题?

  • 拆分 Context:按业务分离(用户Context/主题Context)
  • 使用 React.memo:防止无关组件重渲染
    const Child = React.memo(({ nonContextProp }) => { ... });
  • 选择订阅:使用 use-context-selector 库
    import { useContextSelector } from 'use-context-selector';
    const theme = useContextSelector(ThemeContext, ctx => ctx.theme);

Q:事件总线会导致什么问题?

  • 内存泄漏:组件卸载时取消订阅
    useEffect(() => {
      const handler = () => {...};
      bus.on('event', handler);
      return () => bus.off('event', handler); // ✅
    }, []);
  • 事件冲突:使用命名空间
    bus.emit('moduleA:event', data);
  • 调试困难:添加事件日志
    function emit(event, data) {
      console.log(`[Event] ${event}`, data);
      // ...原逻辑
    }

2.5 Refs 使用场景(DOM 操作、forwardRef)

Refs 核心概念全景图

graph TD
  A[Refs 类型] --> B[对象 Ref]
  A --> C[回调 Ref]
  A --> D[函数组件 useRef]
  
  E[使用场景] --> F[访问 DOM 节点]
  E --> G[访问类组件实例]
  E --> H[存储可变值]
  E --> I[集成第三方库]
  
  J[高级特性] --> K[forwardRef 转发]
  J --> L[useImperativeHandle 暴露方法]

三大 Refs 创建方式

对象 Ref(类组件)

class AutoFocusInput extends React.Component {
  inputRef = React.createRef();  // 创建 Ref 对象
  
  componentDidMount() {
    this.inputRef.current.focus();  // 访问 DOM 节点
  }
  
  render() {
    return <input ref={this.inputRef} />;
  }
}

回调 Ref(动态场景)

class DynamicRef extends React.Component {
  setRef = (element) => {
    if (element) {
      element.focus();  // 元素挂载时执行
    }
  };
  
  render() {
    return <input ref={this.setRef} />;
  }
}

useRef Hook(函数组件)

function FocusInput() {
  const inputRef = useRef(null);  // 创建 Ref
  
  useEffect(() => {
    inputRef.current.focus();  // 挂载后聚焦
  }, []);
  
  return <input ref={inputRef} />;
}

核心使用场景深度解析

DOM 操作(核心场景)

function VideoPlayer() {
  const videoRef = useRef(null);
  
  const play = () => videoRef.current.play();
  const pause = () => videoRef.current.pause();
  
  return (
    <div>
      <video ref={videoRef} src="movie.mp4" />
      <button onClick={play}>播放</button>
      <button onClick={pause}>暂停</button>
    </div>
  );
}

存储可变值(不触发重渲染)

function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef();  // 存储计时器 ID
  
  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    
    return () => clearInterval(intervalRef.current);
  }, []);
  
  // 停止计时器(不依赖 state)
  const stop = () => clearInterval(intervalRef.current);
  
  return <div>{count} <button onClick={stop}>停止</button></div>;
}

类组件方法调用

// 子组件(类组件)
class AudioPlayer extends React.Component {
  play = () => this.audio.play();
  pause = () => this.audio.pause();
  
  render() {
    return <audio ref={el => this.audio = el} />;
  }
}

// 父组件
function Parent() {
  const playerRef = useRef();
  
  return (
    <>
      <AudioPlayer ref={playerRef} />
      <button onClick={() => playerRef.current.play()}>播放</button>
    </>
  );
}

第三方库集成

function ChartContainer() {
  const chartRef = useRef();
  const chartInstance = useRef();
  
  useEffect(() => {
    // 初始化图表
    chartInstance.current = new ThirdPartyChart(chartRef.current, data);
    
    return () => chartInstance.current.destroy();  // 清理
  }, [data]);
  
  return <div ref={chartRef} style={{ height: '400px' }} />;
}

forwardRef 转发机制

问题背景:函数组件不能直接使用 ref

// ❌ 错误用法:函数组件无实例
const MyInput = ({ value }) => <input value={value} />;

function Parent() {
  const inputRef = useRef();
  // 无效!inputRef.current 将为 null
  return <MyInput ref={inputRef} />;
}

forwardRef 解决方案

const MyInput = React.forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

function Parent() {
  const inputRef = useRef();
  
  useEffect(() => {
    inputRef.current.focus();  // ✅ 现在可以操作 DOM
  }, []);
  
  return <MyInput ref={inputRef} />;
}

转发多个 ref

const MultiInput = React.forwardRef((props, ref) => {
  const input1Ref = useRef();
  const input2Ref = useRef();
  
  // 合并 ref 到父组件
  useImperativeHandle(ref, () => ({
    focusFirst: () => input1Ref.current.focus(),
    focusSecond: () => input2Ref.current.focus()
  }));
  
  return (
    <>
      <input ref={input1Ref} />
      <input ref={input2Ref} />
    </>
  );
});

// 父组件使用
function Parent() {
  const inputsRef = useRef();
  
  return (
    <>
      <MultiInput ref={inputsRef} />
      <button onClick={() => inputsRef.current.focusFirst()}>聚焦输入1</button>
    </>
  );
}

useImperativeHandle 高级用法

暴露自定义方法

const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    // 暴露特定方法
    focus: () => {
      inputRef.current.focus();
    },
    scrollIntoView: () => {
      inputRef.current.scrollIntoView();
    },
    // 限制访问 DOM 节点
    getValue: () => inputRef.current.value
  }));
  
  return <input ref={inputRef} {...props} />;
});

// 父组件
function Parent() {
  const inputRef = useRef();
  
  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>聚焦</button>
      <button onClick={() => console.log(inputRef.current.getValue())}>获取值</button>
    </>
  );
}

依赖项控制

useImperativeHandle(ref, () => ({
  // 当 value 变化时更新方法
  getValue: () => inputRef.current.value
}), [props.value]);  // 依赖项数组

高频面试题与破解方案

Q:为什么函数组件默认不能使用 ref?

  • 函数组件无实例,ref 无法指向组件对象
  • React 设计哲学:避免直接操作子组件
  • 解决方案:forwardRef + useImperativeHandle

Q:useRef 和 useState 有何区别?

特性 useRef useState
触发重渲染
存储值类型 可变对象(.current) 不可变状态
数据持久化 组件生命周期 状态更新保留
典型用途 DOM 引用/计时器 ID 渲染相关状态

Q:回调 Ref 有何特殊用途?

  • 动态绑定:可在运行时切换 ref 目标
    <input ref={condition ? ref1 : ref2} />
  • 精细控制:元素挂载/卸载时执行逻辑
    const measureRef = (el) => {
      if (el) {
        console.log('尺寸:', el.getBoundingClientRect());
      }
    };

总结:Refs 使用决策

graph TD
  A[是否需要操作 DOM?] -->|是| B[使用 Ref]
  A -->|否| C[考虑 State]
  B --> D{组件类型?}
  D -->|函数组件| E[useRef + forwardRef]
  D -->|类组件| F[createRef]
  B --> G{需要暴露方法?}
  G -->|是| H[useImperativeHandle]
  G -->|否| I[直接访问 DOM]

第三章:Hooks 深度解析

React Hooks 使用规则两大核心铁律:

  • 只在 React 函数顶层使用 Hook
    • 不能在循环、条件或嵌套函数中调用 Hook
  • 只在 React 函数组件或自定义 Hook 中调用
    • 不能在普通 JS 函数、类组件或事件处理函数中调用

规则背后的原理:Hook 依赖调用顺序

React 内部使用调用顺序链表管理 Hook 状态:

graph LR
    A[首次渲染] --> B[useState1] 
    B --> C[useEffect2]
    C --> D[useState3]
    
    E[二次渲染] --> F[useState1] 
    F --> G[useEffect2]
    G --> H[useState3]
    
    I[条件渲染] --> J[useState1]
    J --> K{condition}
    K -->|true| L[useState3]
  • 为什么重要:React 依靠 Hook 的稳定调用顺序将状态与特定 Hook 关联
  • 条件调用破坏:如果某次渲染跳过了 Hook,后续 Hook 的关联关系将错乱
    // 错误示例:条件渲染返回不同数量 Hook
    return isLoading 
      ? <Loader /> // 🚫 无 Hook
      : <Content useHook={true} />; // 🚫 有 Hook

Hook 规则核心要点:

规则 正确示例 错误示例
顶层调用 组件函数顶部直接调用 条件/循环内调用
顺序一致性 每次渲染相同数量/顺序的 Hook 动态增减 Hook
限定作用域 React 函数组件或自定义 Hook 类组件/普通函数中使用
依赖数组声明 明确声明所有外部依赖 忽略依赖导致闭包问题
自定义 Hook 命名 useCustomHook 格式 getCustomHook 格式

3.1 useState 与状态管理

基础概念概述

useState 作用:函数组件中添加状态能力的 Hook,替代类组件的 this.state

基本语法:

const [state, setState] = useState(initialValue);
  • state:当前状态值
  • setState:状态更新函数
  • initialValue:初始状态(支持惰性初始化)

核心机制深度解析

闭包状态快照原理

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

  const increment = () => {
    setCount(count + 1);   // 闭包捕获当前 count 值
    
    console.log(count);    // 输出旧值(状态更新是异步的)
  };
  
  return <button onClick={increment}>{count}</button>;
}

关键特性:

  • 每次渲染都有独立的函数作用域
  • 状态更新函数调用时使用当次渲染的快照值
  • 状态更新触发重新渲染(生成新的闭包)

函数式更新解决闭包陷阱

// ❌ 闭包陷阱(连续点击只增加1)
setCount(count + 1);
setCount(count + 1);

// ✅ 函数式更新(连续点击增加2)
setCount(prev => prev + 1);
setCount(prev => prev + 1);

原理:更新函数接收 previous state 参数,React 保证这是最新状态

状态批处理机制(React 18+)

const handleClick = () => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 18:自动批处理为单次渲染
};

事件循环中的表现:

sequenceDiagram
  participant UI as 用户界面
  participant React as React 运行时
  UI->>React: 触发事件
  React->>React: 执行所有 setState
  React->>React: 合并状态更新
  React->>UI: 单次重渲染

惰性初始化(Lazy Initialization)

// ✅ 避免重复计算初始值
const [state, setState] = useState(() => {
  const initialState = computeExpensiveValue(props);
  return initialState;
});
  • 初始化函数仅在挂载时执行一次
  • 避免重新渲染时重复计算

状态管理进阶模式

状态依赖更新

useEffect(() => {
  // 当 count 变化时重置 page
  setPage(1);
}, [count]); // 依赖项触发状态重置

状态提升与下沉策略

// 状态提升:共享状态到共同祖先
function Parent() {
  const [sharedState, setSharedState] = useState(null);
  return (
    <ChildA state={sharedState} setState={setSharedState} />
    <ChildB state={sharedState} setState={setSharedState} />
  );
}

// 状态下沉:缩小状态影响范围
function Component() {
  // 只有按钮依赖 count
  const [count, setCount] = useState(0);
  return (
    <ExpensiveComponent />
    <Button count={count} onClick={() => setCount(c => c + 1)} />
  );
}

状态合并模式

// 方案1:使用对象
const [state, setState] = useState({ count: 0, flag: false });
setState(prev => ({ ...prev, count: prev.count + 1 }));

// 方案2:多个 useState
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

// 方案3:useReducer(复杂状态)
const [state, dispatch] = useReducer(reducer, { count: 0, flag: false });

高频面试题与破解方案

Q:为什么连续调用 setState 不更新?

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1); // 本次渲染 count=0
  setCount(count + 1); // 本次渲染 count=0
  // 结果:count 只增加1
};

闭包捕获的 count 是当次渲染的固定值,解决方案:

// ✅ 函数式更新
setCount(prev => prev + 1);
setCount(prev => prev + 1);

Q:useState 和 useRef 存储数据有何区别?

特性 useState useRef
触发重渲染
数据持久化 跨渲染保留 跨渲染保留
数据访问 直接使用 state 通过 .current 访问
更新方式 setState 函数 直接修改 .current
典型用途 驱动 UI 变化的状态 DOM 引用/不触发渲染的值

Q:如何实现状态重置?

const initialUser = { name: 'John', age: 30 };

function Profile() {
  const [user, setUser] = useState(initialUser);
  
  const reset = () => {
    // ✅ 正确:重置为初始值
    setUser(initialUser);
    
    // ❌ 危险:可能导致引用问题
    // setUser({ name: 'John', age: 30 });
  };
}

性能优化技巧

避免不必要状态

// ❌ 冗余状态(可从 props 派生)
const [fullName, setFullName] = useState(`${name} ${surname}`);

// ✅ 直接用计算值
const fullName = `${name} ${surname}`;

状态隔离减少重渲染

// 原始结构:状态变化导致整个组件重渲染
const [state, setState] = useState({ a: 1, b: 2 });

// 优化方案:拆分为独立状态
const [a, setA] = useState(1);
const [b, setB] = useState(2);

状态记忆化

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ 避免子组件不必要重渲染
  const child = useMemo(
    () => <ExpensiveChild count={count} />,
    [count] // 仅 count 变化时更新
  );
  
  return <div>{child}</div>;
}

与类组件状态对比

特性 useState this.state
更新方式 独立 set 函数 this.setState({})
状态合并 不自动合并 自动浅合并
异步行为 批处理更新 批处理更新
函数式更新 setCount(prev => prev+1) this.setState(prev => {})
初始化 支持惰性初始化函数 构造函数中初始化

React 18 新特性:自动批处理

批处理范围扩展

// React 17:只在事件处理函数中批处理
setTimeout(() => {
  setCount(1);  // 立即渲染
  setFlag(true); // 再次渲染
}, 100);

// React 18:自动批处理所有更新
setTimeout(() => {
  setCount(1);   // 合并更新
  setFlag(true); // 单次渲染
}, 100);

紧急更新与非紧急更新

import { startTransition } from 'react';

// 紧急更新(用户输入)
setInputValue(input);

// 标记非紧急更新(搜索结果)
startTransition(() => {
  setSearchQuery(input);
});

3.2 useEffect 与副作用(依赖数组、清理函数)

基础概念概述

useEffect 作用:处理副作用操作:数据获取、订阅管理、手动 DOM 操作等

核心参数:

useEffect(() => {
  // 副作用逻辑
  return () => { /* 清理函数 */ };
}, [dependencies]); // 依赖数组

执行时机:

  • 组件挂载后(componentDidMount)
  • 依赖项变化时(componentDidUpdate)
  • 组件卸载前执行清理(componentWillUnmount)

依赖数组深度解析

三种依赖模式对比

依赖数组 执行时机 常见用途
[] 仅挂载时执行一次 初始化请求、事件订阅
[dep1, dep2] 依赖变化时执行 数据请求、状态联动
无依赖 每次渲染后都执行 极少使用(性能危险)

依赖项优化技巧

// ❌ 错误:缺少必要依赖
useEffect(() => {
  fetchData(user.id); 
}, []); // 当 user.id 变化时不会重新请求

// ✅ 正确:包含所有依赖
useEffect(() => {
  fetchData(user.id);
}, [user.id]); 

// ✅ 高级:函数式更新避免依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // 不依赖 count
  }, 1000);
  return () => clearInterval(timer);
}, []); // 空依赖安全

依赖引用陷阱

// 问题:对象引用变化导致无限循环
const config = { timeout: 1000 };

useEffect(() => {
  startTimer(config);
}, [config]); // 每次渲染 config 都是新对象

// ✅ 解决方案:useMemo 稳定引用
const config = useMemo(() => ({ timeout: 1000 }), []);

清理函数高级用法

执行机制

sequenceDiagram
  participant Mount as 组件挂载
  participant Update as 依赖更新
  participant Unmount as 组件卸载
  Mount->>+Effect: 执行副作用
  Update->>+Cleanup: 执行旧清理函数
  Update->>+Effect: 执行新副作用
  Unmount->>+Cleanup: 执行清理函数

常见清理场景

// 1. 清除定时器
useEffect(() => {
  const timer = setInterval(/*...*/);
  return () => clearInterval(timer);
}, []);

// 2. 取消网络请求
useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal });
  
  return () => controller.abort(); // 取消未完成请求
}, [url]);

// 3. 移除事件监听
useEffect(() => {
  const handleResize = () => {/*...*/};
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

清理函数闭包特性

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count); // 总是初始值(闭包陷阱)
  }, 1000);
  
  return () => clearInterval(timer);
}, []);

// ✅ 解决方案:使用 ref 保存最新值
const countRef = useRef(count);
countRef.current = count;

useEffect(() => {
  const timer = setInterval(() => {
    console.log(countRef.current); // 最新值
  }, 1000);
  return () => clearInterval(timer);
}, []);

高频面试题与破解方案

Q:如何避免 useEffect 无限循环?

常见原因:

  • 副作用内更新依赖项状态
    // ❌ 危险:更新状态触发重渲染
    useEffect(() => {
      setCount(count + 1); 
    }, [count]); // 每次count变化又触发
  • 依赖引用类型未优化
    // ❌ 对象字面量导致无限循环
    useEffect(() => {
      // ...
    }, [{ data }]); // 每次新对象
    • 解决方案:
    // ✅ 方案1:检查依赖链,移除不必要的状态更新
    useEffect(() => {
      if (needsUpdate) fetchData(); // 条件执行
    }, [data]);
    
    // ✅ 方案2:使用 useMemo 稳定依赖
    const stableData = useMemo(() => data, [data.id]);
    useEffect(() => {/*...*/}, [stableData]);

Q:为什么严格模式下 useEffect 执行两次?

React 18 严格模式行为:

  • 开发环境故意双调用组件(挂载 → 卸载 → 挂载)
  • 目的:暴露未正确清理的副作用
  • 生产环境无此行为

正确应对:

useEffect(() => {
  const connection = createConnection();
  connection.connect();
  
  return () => connection.disconnect(); // ✅ 确保完全清理
}, []);

Q:useEffect 和 useLayoutEffect 有何区别?

特性 useEffect useLayoutEffect
执行时机 浏览器绘制后异步执行 DOM 更新后,绘制前同步执行
阻塞渲染
使用场景 数据获取、订阅等 DOM 测量、同步样式变更
服务端渲染 正常执行 警告(需用 useEffect 替代)

自定义 Hook 封装

通用数据请求 Hook

function useFetch(url, initialData) {
  const [data, setData] = useState(initialData);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    let isActive = true;
    
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (isActive) setData(await response.json());
      } finally {
        if (isActive) setLoading(false);
      }
    };
    
    fetchData();
    
    return () => { isActive = false };
  }, [url]);
  
  return { data, loading };
}

// 使用
const { data, loading } = useFetch('/api/user');

事件监听 Hook

function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef();
  
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  
  useEffect(() => {
    const eventListener = e => savedHandler.current(e);
    element.addEventListener(eventName, eventListener);
    
    return () => element.removeEventListener(eventName, eventListener);
  }, [eventName, element]);
}

3.3 useContext 与全局状态共享

基础概念概述

useContext 作用:解决组件跨层级通信问题,避免 Props drilling(属性钻探)

核心三要素:

  • React.createContext():创建上下文对象
  • <Context.Provider>:提供数据
  • useContext():消费数据

典型场景:主题切换、用户认证、多语言、全局配置等跨组件共享数据

上下文工作流程详解

graph TD
  A[创建Context] --> B[Provider 提供数据]
  B --> C[子组件消费数据]
  C --> D{消费方式}
  D --> E[useContext Hook]
  D --> F[Context.Consumer]
  D --> G[Class.contextType]

完整使用示例:

// 1. 创建上下文
const ThemeContext = React.createContext('light'); // 默认值

// 2. Provider 提供数据
function App() {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <Content />
    </ThemeContext.Provider>
  );
}

// 3. 函数组件消费
function Header() {
  const { theme } = useContext(ThemeContext);
  return <header className={theme}>标题</header>;
}

// 4. 类组件消费
class Content extends React.Component {
  static contextType = ThemeContext; // 类组件绑定
  
  render() {
    const { theme } = this.context;
    return <main className={theme}>内容</main>;
  }
}

高级应用模式

自定义 Hook 封装

// 创建自定义上下文 Hook
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return context;
}

// 在组件中使用
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme} className={theme}>
      切换主题
    </button>
  );
}

高频面试题与破解方案

Q:Context 与 Redux 如何选择?

决策矩阵:

考量因素 Context Redux
使用复杂度 简单 复杂(action/reducer)
性能优化 需手动优化 内置精细更新
中间件支持 不支持 支持(thunk/saga)
调试工具 Redux DevTools
适用场景 低频更新数据(主题/用户) 高频更新/复杂状态逻辑

总结:

  • 中小应用:Context + useReducer
  • 大型应用:Redux/Zustand

Q:为什么 Context 会导致不必要的重渲染?

根本原因:

Context 使用 值比较(value comparison),当 Provider 的 value 变化时,所有消费该 Context 的组件都会重渲染,无论是否使用变化的部分。

解决方案:

// 1. 拆分 Context(如前文示例)
// 2. 使用 React.memo 优化子组件
const ExpensiveComponent = React.memo(() => { ... });

// 3. 使用选择器库(use-context-selector)

Q:如何检测 Context 未提供?

// 创建时设置默认值(生产环境有用)
const ThemeContext = createContext('light');

// 自定义 Hook 添加检测
function useTheme() {
  const context = useContext(ThemeContext);
  
  if (context === undefined) { // 未提供时使用默认值
    return 'light';
  }
  
  return context;
}

3.4 useReducer 与复杂状态逻辑

基础概念概述

useReducer 作用:管理复杂状态逻辑的 Hook,是 useState 的替代方案,借鉴 Redux 的核心思想

核心参数:

const [state, dispatch] = useReducer(reducer, initialState, initFunc);
  • reducer:状态更新函数 (state, action) => newState
  • initialState:初始状态
  • initFunc:惰性初始化函数(可选)

三大要素:

  • State:应用的状态数据
  • Action:描述状态变化的普通对象
  • Dispatch:触发状态更新的函数 dispatch(action)

核心机制深度解析

Reducer 函数工作原理

// 典型 reducer 结构
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'decrement':
      return { ...state, count: state.count - 1 };
    case 'set_user':
      return { ...state, user: action.payload };
    default:
      return state; // 必须返回默认状态
  }
}

状态更新流程

sequenceDiagram
  participant Component as 组件
  participant Reducer as Reducer函数
  participant React as React运行时
  
  Component->>Reducer: dispatch({ type: 'increment' })
  Reducer->>Reducer: 计算新状态 (state + 1)
  Reducer->>React: 返回新状态
  React->>Component: 触发重渲染

与 useState 对比

特性 useReducer useState
适用场景 复杂状态逻辑 简单状态
状态结构 对象/复杂结构 任意类型
更新逻辑 集中管理(reducer) 分散在组件中
性能优化 容易避免不必要更新 需手动优化
测试 纯函数易于测试 需渲染组件测试

高级应用模式

中间件模式(类似 Redux)

// 增强 dispatch 函数
function useReducerWithMiddleware(reducer, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // 支持中间件链
  const dispatchWithMiddleware = action => {
    console.log('Action:', action); // 日志中间件
    dispatch(action); // 继续传递
  };
  
  return [state, dispatchWithMiddleware];
}

复杂状态管理实战

表单状态管理

function formReducer(state, action) {
  switch (action.type) {
    case 'change_field':
      return {
        ...state,
        [action.field]: action.value,
        touched: { ...state.touched, [action.field]: true }
      };
    case 'validate':
      return { ...state, errors: validateForm(state) };
    case 'submit':
      return { ...state, isSubmitting: true };
    case 'submit_success':
      return { ...initialState, submitSuccess: true };
    default:
      return state;
  }
}

function Form() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const handleChange = (e) => {
    dispatch({
      type: 'change_field',
      field: e.target.name,
      value: e.target.value
    });
  };
  
  // ...
}

异步操作管理

function asyncReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { loading: false, data: action.payload, error: null };
    case 'FETCH_FAILURE':
      return { loading: false, data: null, error: action.error };
    default:
      return state;
  }
}

function UserProfile({ userId }) {
  const [state, dispatch] = useReducer(asyncReducer, {
    loading: false,
    data: null,
    error: null
  });

  useEffect(() => {
    const fetchUser = async () => {
      dispatch({ type: 'FETCH_START' });
      try {
        const data = await fetchUser(userId);
        dispatch({ type: 'FETCH_SUCCESS', payload: data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE', error });
      }
    };
    
    fetchUser();
  }, [userId]);
  
  // ...
}

状态机模式(XState 思想)

function stateMachineReducer(state, action) {
  switch (state.status) {
    case 'idle':
      if (action.type === 'FETCH') return { status: 'loading' };
      return state;
    case 'loading':
      if (action.type === 'SUCCESS') return { status: 'success', data: action.data };
      if (action.type === 'FAILURE') return { status: 'error', error: action.error };
      return state;
    case 'success':
      if (action.type === 'RESET') return { status: 'idle' };
      return state;
    case 'error':
      if (action.type === 'RETRY') return { status: 'loading' };
      return state;
    default:
      return state;
  }
}

高频面试题与破解方案

Q:何时该用 useReducer 而不是 useState?

决策树:

graph TD
  A[状态结构复杂?] -->|是| B[useReducer]
  A -->|否| C{状态更新逻辑复杂?}
  C -->|是| B
  C -->|否| D[useState]
  E[需要跨组件更新?] -->|是| B
  F[需要可预测状态?] -->|是| B

Q:如何避免 reducer 函数过大?

优化策略:

  • 拆分 reducer:
    function rootReducer(state, action) {
      return {
        user: userReducer(state.user, action),
        posts: postsReducer(state.posts, action)
      };
    }
  • 组合 action 创建函数:
    const actions = {
      increment: () => ({ type: 'increment' }),
      addTodo: text => ({ type: 'add_todo', payload: text })
    };
    // 使用:dispatch(actions.increment())

Q:useReducer 如何实现时间旅行调试?

// 1. 添加历史记录
function reducerWithHistory(state, action) {
  const nextState = reducer(state.current, action);
  return {
    current: nextState,
    history: [...state.history, { action, state: nextState }]
  };
}

// 2. 实现撤销/重做
function undoableReducer(state, action) {
  if (action.type === 'UNDO') {
    return { ...state, current: state.history[state.history.length - 2] };
  }
  // ...其他action处理
}

// 使用
const [state, dispatch] = useReducer(undoableReducer, {
  current: initialState,
  history: [initialState]
});

与 Context 集成

全局状态管理方案

// 创建 Context
const StateContext = createContext();
const DispatchContext = createContext();

// 全局 Provider
function AppProvider({ children }) {
  const [state, dispatch] = useReducer(rootReducer, initialState);
  
  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        {children}
      </DispatchContext.Provider>
    </StateContext.Provider>
  );
}

// 自定义 Hook 访问
function useAppState() {
  const state = useContext(StateContext);
  if (!state) throw new Error('必须在 AppProvider 内使用');
  return state;
}

function useAppDispatch() {
  const dispatch = useContext(DispatchContext);
  if (!dispatch) throw new Error('必须在 AppProvider 内使用');
  return dispatch;
}

// 组件中使用
function Counter() {
  const state = useAppState();
  const dispatch = useAppDispatch();
  
  return (
    <button onClick={() => dispatch({ type: 'increment' })}>
      {state.count}
    </button>
  );
}

最佳实践总结

  1. 纯函数原则:Reducer 必须是纯函数,不产生副作用
  2. 不可变更新:始终返回新状态对象,不修改原状态
  3. 类型安全:使用 TypeScript 强化 action 类型
    type Action = 
      | { type: 'increment' }
      | { type: 'decrement' }
      | { type: 'set_count'; payload: number };
  4. 逻辑复用:提取 reducer 到独立文件便于测试
  5. 合理拆分:大型应用按功能域拆分多个 reducer
  6. 避免过度:简单状态仍使用 useState

使用场景决策逻辑

graph TD
  A[状态管理需求] --> B{状态结构}
  B -->|简单值| C[useState]
  B -->|复杂对象| D{更新逻辑}
  D -->|简单| C
  D -->|复杂| E[useReducer]
  A --> F{状态共享范围}
  F -->|组件内| G[useState/useReducer]
  F -->|跨组件| H[Context + useReducer]
  A --> I{需要时间旅行/撤销}
  I -->|是| E

3.5 自定义 Hooks(封装可复用逻辑)

基础概念概述

自定义 Hook 是什么:将组件逻辑提取到可重用的 JavaScript 函数中的技术,是 React 逻辑复用的终极解决方案

核心特征:

  • 名称以 use 开头(如 useFetch)
  • 可以调用其他 Hook(如 useState, useEffect)
  • 不包含 UI 渲染,只包含状态逻辑

与普通函数的区别:

特性 自定义 Hook 工具函数
可调用其他 Hook
状态隔离 每次调用独立状态 无状态/静态
使用场景 组件逻辑复用 数据处理/工具类

核心机制深度解析

状态隔离原理

// 自定义 Hook
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  const increment = () => setCount(c => c + 1);
  return [count, increment];
}

// 组件A使用
function ComponentA() {
  const [countA, incA] = useCounter(0); // 独立状态
}

// 组件B使用
function ComponentB() {
  const [countB, incB] = useCounter(10); // 独立状态
}

关键原理:每次调用自定义 Hook 都会创建独立的状态副本(闭包机制)

Hook 调用规则

graph LR
  A[函数组件] --> B[调用自定义Hook]
  B --> C[内部调用React Hook]
  C --> D[返回状态和逻辑]
  D --> A

强制规则:

  • 只在 React 函数组件或自定义 Hook 中调用
  • 只在顶层调用(不能在条件/循环中使用)
  • 命名必须以 use 开头

设计模式与最佳实践

参数传递与返回值

// 灵活的参数设计
function useToggle(initial = false) {
  const [state, setState] = useState(initial);
  
  // 支持直接设置或切换
  const toggle = (value) => {
    if (typeof value === 'boolean') {
      setState(value);
    } else {
      setState(prev => !prev);
    }
  };
  
  // 返回状态和控制方法
  return [state, toggle];
}

// 使用
const [isOpen, toggleOpen] = useToggle(false);

组合 Hook 模式

// 组合多个 Hook 创建复杂逻辑
function useUserProfile(userId) {
  const [user, loading, error] = useFetch(`/api/users/${userId}`);
  const [preferences, setPrefs] = useLocalStorage(`prefs_${userId}`, {});
  const [notifications, addNotification] = useNotifications();
  
  return {
    user,
    preferences,
    setPreferences: setPrefs,
    notifications,
    addNotification,
    loading,
    error
  };
}

依赖注入模式

// 创建可配置的自定义 Hook
function useAPI(endpoint, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  
  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const response = await fetch(endpoint, options);
      setData(await response.json());
    } finally {
      setLoading(false);
    }
  }, [endpoint, options]);
  
  useEffect(() => {
    fetchData();
  }, [fetchData]);
  
  return { data, loading, refetch: fetchData };
}

// 使用
const userAPI = useAPI('/api/users', { method: 'GET' });

高频面试题与破解方案

Q:自定义 Hook 和 HOC(高阶组件)/Render Props 有何区别?

特性 自定义 Hook HOC Render Props
逻辑复用方式 直接调用 Hook 包装组件 通过 props 渲染
组件层级 无额外层级 增加包装层级 增加回调层级
命名冲突 可能发生 可能发生
调试难度 简单 组件树变深 回调嵌套复杂
TypeScript 支持 优秀 类型推导复杂 类型推导中等

结论:
自定义 Hook 是 React 官方推荐的逻辑复用方案,解决了 HOC 和 Render Props 的嵌套问题

Q:如何测试自定义 Hook?

测试策略:

// 使用 @testing-library/react-hooks
import { renderHook, act } from '@testing-library/react-hooks';

test('should use counter', () => {
  const { result } = renderHook(() => useCounter());
  
  expect(result.current[0]).toBe(0); // 初始值
  
  act(() => {
    result.current[1](); // 执行 increment
  });
  
  expect(result.current[0]).toBe(1); // 更新后值
});

Q:自定义 Hook 会导致内存泄漏吗?

风险与防范:

function useEventListener(eventName, handler, element = window) {
  const savedHandler = useRef();
  
  // 保存最新处理函数
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  
  useEffect(() => {
    const eventListener = event => savedHandler.current(event);
    element.addEventListener(eventName, eventListener);
    
    // ✅ 必须返回清理函数
    return () => element.removeEventListener(eventName, eventListener);
  }, [eventName, element]); // 依赖变化时重新绑定
}

实践案例

权限控制 Hook

function usePermission(requiredPermission) {
  const [user] = useAuth();
  const [hasPermission, setHasPermission] = useState(false);
  
  useEffect(() => {
    if (!user) return;
    
    // 异步检查权限(如调用API)
    checkPermission(user.id, requiredPermission).then(setHasPermission);
  }, [user, requiredPermission]);
  
  return hasPermission;
}

// 使用
function AdminPanel() {
  const canEdit = usePermission('admin_edit');
  
  return canEdit ? <Editor /> : <PermissionDenied />;
}

状态机 Hook

function useStateMachine(states, initialState) {
  const [state, setState] = useState(initialState);
  
  const transition = useCallback((event) => {
    const currentState = state;
    const nextState = states[currentState]?.[event];
    
    if (!nextState) {
      console.error(`无效转换: ${currentState} -> ${event}`);
      return;
    }
    
    setState(nextState);
  }, [state, states]);
  
  return [state, transition];
}

// 使用
const [state, transition] = useStateMachine({
  idle: { START: 'running' },
  running: { PAUSE: 'paused', COMPLETE: 'done' },
  paused: { RESUME: 'running', CANCEL: 'idle' },
  done: { RESET: 'idle' }
}, 'idle');

自定义 Hook 设计原则

  1. 单一职责:一个 Hook 只解决一个问题
  2. 命名清晰:use + 功能名(useWindowSize)
  3. 参数合理:提供默认值和必要配置
  4. 返回简洁:返回数组或对象结构
  5. 文档完善:使用 JSDoc 说明用法
  6. 类型安全:TypeScript 类型定义

总结:自定义 Hook 价值

graph TD
  A[逻辑复用] --> B[减少代码重复]
  A --> C[统一业务逻辑]
  D[关注点分离] --> E[提升可维护性]
  D --> F[便于单元测试]
  G[组件简化] --> H[提升可读性]
  G --> I[降低复杂度]

3.6 其他常用 Hooks(useMemo、useCallback、useRef)

三大 Hook 快速对比

Hook 主要作用 返回值 性能优化原理
useMemo 缓存计算结果 记忆化值 避免重复计算
useCallback 缓存函数引用 记忆化函数 避免子组件不必要重渲染
useRef 保存可变值/DOM 引用 { current: value } 不触发重渲染

useMemo 深度解析

核心语法

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 计算函数:返回需要缓存的值
  • 依赖数组:当依赖变化时重新计算

工作原理

graph LR
  A[组件渲染] --> B{依赖是否变化?}
  B -->|否| C[返回缓存值]
  B -->|是| D[执行计算函数]
  D --> E[缓存新值]
  E --> F[返回新值]

使用场景

// 场景1:高开销计算
function Component({ items }) {
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.value - b.value); // 排序开销大
  }, [items]);
  
  return <List items={sortedItems} />;
}

// 场景2:避免不必要重渲染
function Parent({ user }) {
  const userContext = useMemo(() => ({ 
    user, 
    isAdmin: user.role === 'admin' 
  }), [user]); // 依赖变化时重建
  
  return <Child context={userContext} />;
}

错误用法

// ❌ 错误1:没有依赖数组(每次渲染都计算)
const value = useMemo(() => compute());

// ❌ 错误2:依赖数组不全
const fullName = useMemo(() => `${name} ${surname}`, [name]);

useCallback 深度解析

核心语法

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

与 useMemo 关系

// useCallback 等价于:
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);

使用场景

// 场景1:避免子组件不必要重渲染
function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ 稳定函数引用
  const increment = useCallback(() => setCount(c => c + 1), []);
  
  return <Child onClick={increment} />;
}

const Child = React.memo(({ onClick }) => {
  // 依赖稳定函数引用不会重渲染
  return <button onClick={onClick}>+</button>;
});

// 场景2:依赖数组中的函数
function Form() {
  const [text, setText] = useState('');
  
  const handleSubmit = useCallback(() => {
    console.log('提交:', text);
  }, [text]); // 正确捕获最新text
  
  useEffect(() => {
    const keyHandler = e => {
      if (e.key === 'Enter') handleSubmit();
    };
    
    window.addEventListener('keydown', keyHandler);
    return () => window.removeEventListener('keydown', keyHandler);
  }, [handleSubmit]); // 依赖稳定函数
}

性能陷阱

// ❌ 错误:过度使用 useCallback
const handleClick = useCallback(() => {
  console.log('点击');
}, []); // 简单函数不需要缓存

// ✅ 正确:直接定义函数
const handleClick = () => console.log('点击');

useRef 高级用法(补充 2.5 章节)

保存可变值

function Timer() {
  const countRef = useRef(0); // { current: 0 }
  
  const increment = () => {
    countRef.current += 1;
    console.log('计数:', countRef.current);
  };
  
  // 不触发重渲染!
}

访问上一状态

function Counter() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();
  
  useEffect(() => {
    prevCountRef.current = count; // 渲染后保存当前值
  });
  
  const prevCount = prevCountRef.current;
  
  return (
    <div>
      当前: {count}, 之前: {prevCount}
    </div>
  );
}

跨渲染周期存储

function ChatRoom() {
  const [messages, setMessages] = useState([]);
  const socketRef = useRef();
  
  useEffect(() => {
    socketRef.current = new WebSocket('wss://...');
    socketRef.current.onmessage = (event) => {
      setMessages(msgs => [...msgs, event.data]);
    };
    
    return () => socketRef.current.close();
  }, []);
  
  // 在其他函数中使用
  const sendMessage = (text) => {
    socketRef.current.send(text);
  };
}

高频面试题与破解方案

Q:useMemo 和 useCallback 有何区别?

核心区别:

  • useMemo 缓存计算结果,用于避免重复计算
  • useCallback 缓存函数引用,用于保持引用稳定

等效关系:

useCallback(fn, deps) === useMemo(() => fn, deps)

Q:何时该用 useMemo/useCallback?

graph TD
  A[需要优化?] -->|否| B[直接计算/定义函数]
  A -->|是| C{优化类型?}
  C -->|避免计算开销| D[用useMemo]
  C -->|避免子组件重渲染| E[useCallback+React.memo]
  C -->|稳定依赖项| F[两者皆可]

Q:useRef 和 useState 有何区别?

特性 useRef useState
触发重渲染
存储值类型 可变对象(.current) 不可变状态
数据持久化 组件生命周期 状态更新保留
典型用途 DOM 引用/计时器 ID 渲染相关状态
初始化 useRef(initial) useState(initial)

高级优化模式

虚拟滚动优化

function VirtualList({ items, itemHeight }) {
  const containerRef = useRef();
  const [visibleRange, setVisibleRange] = useState([0, 10]);
  
  // 缓存可见项
  const visibleItems = useMemo(() => {
    return items.slice(visibleRange[0], visibleRange[1]);
  }, [items, visibleRange]);
  
  // 计算总高度(虚拟滚动)
  const totalHeight = useMemo(() => {
    return items.length * itemHeight;
  }, [items.length, itemHeight]);
  
  return (
    <div ref={containerRef} style={{ height: '500px', overflow: 'auto' }}>
      <div style={{ height: `${totalHeight}px`, position: 'relative' }}>
        {visibleItems.map(item => (
          <Item 
            key={item.id} 
            item={item}
            style={{ 
              position: 'absolute',
              top: `${item.index * itemHeight}px` 
            }}
          />
        ))}
      </div>
    </div>
  );
}

防抖/节流优化

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  
  // 使用useMemo避免重复请求
  const results = useMemo(() => {
    return fetchResults(debouncedQuery);
  }, [debouncedQuery]);
  
  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <Results data={results} />
    </div>
  );
}

总结:性能优化决策

graph TD
  A[性能问题?] -->|是| B{问题类型}
  A -->|否| C[无需优化]
  B -->|组件重渲染过多| D[React.memo+useCallback]
  B -->|计算开销大| E[useMemo]
  B -->|DOM操作频繁| F[useRef+useLayoutEffect]
  D --> G[验证优化效果]
  E --> G
  F --> G
  G -->|解决| H[停止]
  G -->|未解决| I[性能分析工具]

第四章:React 高级特性

4.1 性能优化(React.memo、useMemo、useCallback)

性能优化核心原则

React 渲染机制回顾

graph LR
A[状态/Props变化] --> B[组件重新渲染]
B --> C{子组件是否更新?}
C -->|Props变化| D[重新渲染子组件]
C -->|Props未变化| E[跳过子组件渲染]

性能优化核心策略

优化策略 适用场景 实现方式
减少渲染次数 避免不必要的渲染 React.memo、PureComponent
减少计算量 复杂计算/数据转换 useMemo
保持引用稳定 函数/对象作为依赖项 useCallback、useMemo
虚拟化长列表 大数据量渲染 react-window、react-virtualized

核心 API 深度解析

React.memo 详解

// 基础用法
const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});

// 自定义比较函数
const UserProfile = React.memo(({ user, settings }) => {
  return (
    <div>
      <Avatar url={user.avatar} />
      <Preferences options={settings} />
    </div>
  );
}, (prevProps, nextProps) => {
  // 仅在 user.id 或 settings.theme 变化时重新渲染
  return prevProps.user.id === nextProps.user.id && 
         prevProps.settings.theme === nextProps.settings.theme;
});

关键特性:

  • 仅进行浅比较(shallow compare)
  • 比较函数返回 true 表示不需要重新渲染
  • 适用于函数组件

优化场景:

  • 纯展示型组件(无内部状态)
  • 接收复杂对象 Props 的组件
  • 位于渲染频繁组件树中的叶子节点

useMemo 深度应用

function ProductList({ products, filterText, sortBy }) {
  // 缓存复杂计算结果
  const filteredProducts = useMemo(() => {
    console.log('重新计算过滤产品');
    return products.filter(p => 
      p.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [products, filterText]); // 依赖项
  
  // 缓存排序结果
  const sortedProducts = useMemo(() => {
    console.log('重新排序产品');
    return [...filteredProducts].sort((a, b) => {
      if (sortBy === 'price') return a.price - b.price;
      return a.name.localeCompare(b.name);
    });
  }, [filteredProducts, sortBy]);
  
  return (
    <ul>
      {sortedProducts.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

关键特性:

  • 缓存计算结果,避免重复计算
  • 依赖项变化时重新计算
  • 适用于:复杂计算、数据转换、组件记忆化

性能陷阱:

// 错误用法:useMemo 作为语义保证
const data = useMemo(() => fetchData(), []); // 不会阻止重复请求

// 正确用法:仅用于性能优化
const transformedData = useMemo(() => 
  rawData.map(transformItem), 
[rawData]);

useCallback 高级用法

function ParentComponent() {
  const [count, setCount] = useState(0);
  
  // 缓存函数引用
  const handleClick = useCallback(() => {
    console.log('点击事件', count);
    // 注意:闭包陷阱!count 是创建时的值
  }, [count]); // 依赖 count 保证最新值
  
  // 使用 ref 解决闭包问题
  const countRef = useRef(count);
  countRef.current = count;
  
  const stableHandler = useCallback(() => {
    console.log('最新值:', countRef.current);
  }, []); // 无依赖,函数引用稳定
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加 {count}</button>
      <ChildComponent onClick={stableHandler} />
    </div>
  );
}

const ChildComponent = React.memo(({ onClick }) => {
  console.log('子组件渲染');
  return <button onClick={onClick}>触发事件</button>;
});

关键特性:

  • 缓存函数引用,避免子组件不必要渲染
  • 依赖项变化时创建新函数
  • 适用于:事件处理函数、传递给子组件的回调

最佳实践:

// 依赖项处理技巧
const handleSubmit = useCallback(() => {
  api.submitForm(formState);
}, [formState]); // ✅ 正确:依赖实际使用的状态

// 使用函数式更新避免依赖
const increment = useCallback(() => {
  setCount(prev => prev + 1); // ✅ 不依赖 count
}, []);

综合优化策略

组件结构优化

// 优化前:状态提升导致频繁渲染
function Parent() {
  const [state, setState] = useState({});
  return (
    <div>
      <ExpensiveComponent data={state.data} />
      <ControlPanel onChange={setState} />
    </div>
  );
}

// 优化后:状态下沉
function OptimizedParent() {
  return (
    <div>
      <DataContainer>
        {(data) => <ExpensiveComponent data={data} />}
      </DataContainer>
      <ControlPanel />
    </div>
  );
}

function DataContainer({ children }) {
  const [data, setData] = useState(null);
  // 状态管理隔离在此组件
  return children(data);
}

上下文优化技巧

// 创建拆分上下文
const UserContext = React.createContext(null);
const SettingsContext = React.createContext(null);

function App() {
  const [user, setUser] = useState(null);
  const [settings, setSettings] = useState({});
  
  return (
    <UserContext.Provider value={user}>
      <SettingsContext.Provider value={settings}>
        <Header />
        <MainContent />
      </SettingsContext.Provider>
    </UserContext.Provider>
  );
}

// 消费者组件
const ThemeSwitcher = () => {
  // 只订阅需要的上下文
  const settings = useContext(SettingsContext);
  return <div>当前主题: {settings.theme}</div>;
};

// 使用 React.memo 防止不必要渲染
const Header = React.memo(() => {
  const user = useContext(UserContext);
  return <header>{user ? user.name : '游客'}</header>;
});

虚拟化长列表

import { FixedSizeList } from 'react-window';

const BigList = ({ items }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      <ListItem item={items[index]} />
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      width={800}
      itemCount={items.length}
      itemSize={100} // 每项高度
    >
      {Row}
    </FixedSizeList>
  );
};

// 优化列表项
const ListItem = React.memo(({ item }) => {
  return (
    <div className="item">
      <Avatar url={item.avatar} />
      <div>
        <h3>{item.name}</h3>
        <p>{item.description}</p>
      </div>
    </div>
  );
});

性能分析工具

React DevTools Profiler

// 使用示例
function ProfilerTest() {
  return (
    <Profiler id="Navigation" onRender={onRenderCallback}>
      <Navigation />
    </Profiler>
  );
}

function onRenderCallback(
  id, // Profiler 树的 "id"
  phase, // "mount" 或 "update"
  actualDuration, // 本次更新花费的渲染时间
  baseDuration, // 估计不使用 memoization 的渲染时间
  startTime, // 本次更新开始时间
  commitTime, // 提交时间
  interactions // 本次更新所关联的 set
) {
  console.log(`组件 ${id}${phase} 阶段耗时: ${actualDuration}ms`);
}

Chrome Performance Tab

  • 操作步骤:
    • 打开 Chrome DevTools
    • 切换到 Performance 选项卡
    • 点击 Record 开始记录
    • 执行页面操作
    • 停止记录分析火焰图
  • 关键指标:
    • Scripting:JavaScript 执行时间
    • Rendering:样式计算和布局
    • Painting:像素绘制时间
    • Idle:空闲时间

常见性能问题与解决方案

状态频繁变更导致渲染抖动

  • 场景:输入框实时搜索导致频繁渲染
  • 解决方案:
    function SearchBox() {
      const [inputValue, setInputValue] = useState('');
      const [searchResults, setSearchResults] = useState([]);
      
      // 使用防抖
      const debouncedSearch = useMemo(() => 
        debounce(query => {
          api.search(query).then(setSearchResults);
        }, 300),
      []);
      
      useEffect(() => {
        debouncedSearch(inputValue);
        return () => debouncedSearch.cancel();
      }, [inputValue, debouncedSearch]);
      
      return (
        <div>
          <input 
            value={inputValue}
            onChange={e => setInputValue(e.target.value)}
          />
          <ResultsList results={searchResults} />
        </div>
      );
    }

复杂组件树更新缓慢

  • 场景:大型表单组件局部更新
  • 解决方案:状态隔离 + React.memo
    const BigForm = () => {
      const [formData, setFormData] = useState(initialData);
      
      // 状态更新函数
      const updateField = useCallback((field, value) => {
        setFormData(prev => ({ ...prev, [field]: value }));
      }, []);
      
      return (
        <form>
          <PersonalSection 
            data={formData.personal} 
            onChange={updateField} 
          />
          <AddressSection 
            data={formData.address} 
            onChange={updateField} 
          />
          <PaymentSection 
            data={formData.payment} 
            onChange={updateField} 
          />
        </form>
      );
    };
    
    // 优化子组件
    const PersonalSection = React.memo(({ data, onChange }) => {
      return (
        <section>
          <Input 
            label="姓名"
            value={data.name}
            onChange={v => onChange('name', v)}
          />
          {/* 其他字段 */}
        </section>
      );
    }, (prev, next) => {
      // 仅当 personal 部分变化时重新渲染
      return shallowEqual(prev.data, next.data);
    });

动画卡顿问题

  • 解决方案:使用 CSS 动画 + will-change
    const AnimatedBox = ({ position }) => {
      return (
        <div 
          className="animated-box"
          style={{ 
            transform: `translateX(${position}px)`,
            willChange: 'transform' // 提示浏览器优化
          }}
        />
      );
    };
    
    // CSS 中使用 GPU 加速
    .animated-box {
      transition: transform 0.3s ease-out;
      /* 启用 GPU 加速 */
      transform: translateZ(0);
      backface-visibility: hidden;
      perspective: 1000px;
    }

性能优化原则总结

  1. 测量优先:优化前先使用 Profiler 分析性能瓶颈
  2. 避免过早优化:只在必要时应用优化技术
  3. 关注关键路径:优先优化渲染频率高的组件
  4. 合理使用工具:
    • React.memo 用于组件记忆化
    • useMemo 用于昂贵计算缓存
    • useCallback 用于函数引用稳定
  5. 结构优化:
    • 组件职责单一化
    • 状态合理下沉
    • 避免深层嵌套

面试高频问题与解答

Q:React.memo 与 useMemo 有什么区别?

  • React.memo 是高阶组件,用于包装组件,避免不必要的重新渲染
  • useMemo 是Hook,用于在组件内部缓存计算结果
  • 前者优化组件渲染,后者优化计算性能

Q:什么情况下应该使用 useCallback?

  • 当函数作为 props 传递给记忆化组件(React.memo)
  • 当函数作为其他 Hook 的依赖项(如 useEffect)
  • 当函数在渲染中被创建且依赖项未改变时

Q:为什么使用 useMemo/useCallback 后性能反而下降?

  • 过度使用:简单计算使用 useMemo 得不偿失
  • 依赖项错误:依赖项频繁变化导致重新计算
  • 比较成本高:自定义比较函数复杂度高
  • 闭包陷阱:函数闭包持有旧值导致逻辑错误

Q:如何避免 useCallback 的闭包陷阱?

解决方案:

// 方案1:使用函数式更新
const increment = useCallback(() => {
  setCount(prev => prev + 1);
}, []);

// 方案2:使用 ref 保存最新值
const countRef = useRef(count);
countRef.current = count;

const logCount = useCallback(() => {
  console.log(countRef.current);
}, []);

// 方案3:正确声明依赖项
const logCount = useCallback(() => {
  console.log(count);
}, [count]); // 依赖 count

4.2 错误边界(ErrorBoundary)

错误边界核心概念

错误边界解决的问题

graph TD
    A[组件树中的错误] --> B[React 16 前]
    A --> C[React 16+]
    B --> D[整个应用崩溃]
    C --> E[错误边界捕获]
    E --> F[降级UI展示]
    E --> G[错误日志记录]

错误边界能力范围

可捕获 不可捕获
子组件渲染错误 事件处理器错误
生命周期方法错误 异步代码错误
构造函数错误 服务端渲染错误
整个组件树的错误 错误边界自身错误

错误边界实现机制

基础实现方案

class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // 记录错误到监控系统
    logErrorToService(error, errorInfo.componentStack);
    
    // 可选的错误恢复逻辑
    if (isRecoverable(error)) {
      setTimeout(() => this.setState({ hasError: false }), 5000);
    }
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>应用遇到问题</h2>
          <p>{this.state.error.message}</p>
          <button onClick={() => window.location.reload()}>
            重新加载
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 使用示例
function App() {
  return (
    <ErrorBoundary 
      fallback={<CriticalErrorScreen />}
    >
      <MainApplication />
    </ErrorBoundary>
  );
}

高级特性实现

// 带错误恢复功能的边界
class RecoverableErrorBoundary extends React.Component {
  state = { error: null, errorInfo: null, recoveryKey: 0 };
  
  componentDidCatch(error, errorInfo) {
    this.setState({ 
      error, 
      errorInfo,
      recoveryKey: this.state.recoveryKey + 1
    });
  }
  
  handleReset = () => {
    this.setState({ error: null, errorInfo: null });
  };
  
  render() {
    if (this.state.error) {
      return (
        <div>
          <h2>组件崩溃</h2>
          <ErrorDetails 
            error={this.state.error} 
            stack={this.state.errorInfo.componentStack}
          />
          <button onClick={this.handleReset}>
            重置组件
          </button>
        </div>
      );
    }
    
    // 使用 key 强制重置子组件
    return <div key={this.state.recoveryKey}>{this.props.children}</div>;
  }
}

// 错误详情组件
function ErrorDetails({ error, stack }) {
  return (
    <details style={{ whiteSpace: 'pre-wrap' }}>
      <summary>{error.toString()}</summary>
      <pre>{stack}</pre>
    </details>
  );
}

企业级最佳实践

多层错误边界策略

function AppHierarchy() {
  return (
    <ErrorBoundary 
      level="app" 
      fallback={<AppCrashScreen />}
    >
      <Header />
      
      <ErrorBoundary 
        level="billing" 
        fallback={<BillingModuleError />}
      >
        <BillingDashboard />
      </ErrorBoundary>
      
      <ErrorBoundary 
        level="user-profile" 
        fallback={<ProfileError />}
      >
        <UserProfile />
      </ErrorBoundary>
      
      <ErrorBoundary 
        level="notifications" 
        fallback={null} // 静默失败
      >
        <NotificationCenter />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

错误边界与监控系统集成

class MonitoringErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // 发送错误到监控平台
    monitoringClient.captureException(error, {
      extra: {
        componentStack: errorInfo.componentStack,
        userId: getCurrentUser()?.id,
        route: window.location.pathname
      },
      tags: { error_boundary: true }
    });
    
    // 自定义错误处理
    if (this.props.onError) {
      this.props.onError(error, errorInfo);
    }
  }
  
  render() {
    return this.state.hasError 
      ? this.props.fallback 
      : this.props.children;
  }
}

// 使用示例
<MonitoringErrorBoundary
  fallback={<ErrorScreen />}
  onError={(error) => {
    // 特殊错误类型处理
    if (error instanceof PaymentError) {
      router.navigate('/payment-failed');
    }
  }}
>
  <CheckoutProcess />
</MonitoringErrorBoundary>

错误边界与Suspense集成

function AsyncBoundary({ children, fallback, errorFallback }) {
  return (
    <ErrorBoundary fallback={errorFallback}>
      <React.Suspense fallback={fallback}>
        {children}
      </React.Suspense>
    </ErrorBoundary>
  );
}

// 使用示例
<AsyncBoundary 
  fallback={<Spinner />}
  errorFallback={<AsyncError />}
>
  <LazyComponent />
</AsyncBoundary>

函数组件错误处理方案

自定义Hook实现

function useErrorBoundary() {
  const [error, setError] = useState(null);
  
  const handleError = useCallback((err) => {
    setError(err);
    return err;
  }, []);
  
  const resetError = useCallback(() => {
    setError(null);
  }, []);
  
  return [error, handleError, resetError];
}

// 使用示例
function ComponentWithError() {
  const [error, handleError, resetError] = useErrorBoundary();
  
  if (error) {
    return (
      <div>
        <h3>组件出错: {error.message}</h3>
        <button onClick={resetError}>重试</button>
      </div>
    );
  }
  
  return <UnstableComponent onError={handleError} />;
}

function UnstableComponent({ onError }) {
  // 模拟错误
  const throwError = () => {
    try {
      // 可能出错的操作
      riskyOperation();
    } catch (err) {
      onError(err); // 手动触发错误边界
    }
  };
  
  return <button onClick={throwError}>执行危险操作</button>;
}

第三方Hook库

// 使用 react-error-boundary 库
import { useErrorBoundary } from 'react-error-boundary';

function UserProfile() {
  const { showBoundary } = useErrorBoundary();
  
  const loadData = async () => {
    try {
      const data = await fetchUserData();
      // 处理数据...
    } catch (error) {
      showBoundary(error);
    }
  };
  
  return (
    <div>
      <button onClick={loadData}>加载数据</button>
    </div>
  );
}

常见错误场景与解决方案

异步错误捕获

// 错误边界无法捕获的异步错误解决方案
function AsyncErrorHandler() {
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // 全局未捕获错误处理
    const handleUnhandledRejection = (event) => {
      setError(event.reason);
    };
    
    const handleUncaughtError = (event) => {
      setError(event.error);
    };
    
    window.addEventListener('unhandledrejection', handleUnhandledRejection);
    window.addEventListener('error', handleUncaughtError);
    
    return () => {
      window.removeEventListener('unhandledrejection', handleUnhandledRejection);
      window.removeEventListener('error', handleUncaughtError);
    };
  }, []);
  
  if (error) {
    return <GlobalErrorScreen error={error} />;
  }
  
  return this.props.children;
}

// 在应用顶层使用
<AsyncErrorHandler>
  <App />
</AsyncErrorHandler>

错误边界嵌套冲突

// 自定义错误传播机制
class PropagatingErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    if (this.props.onError) {
      // 将错误传递给父级边界
      this.props.onError(error, errorInfo);
    } else if (!this.state.hasError) {
      // 没有父级处理器时处理错误
      console.error('Unhandled error:', error, errorInfo);
    }
  }
  
  render() {
    return this.state.hasError 
      ? this.props.fallback 
      : this.props.children;
  }
}

// 使用示例
<PropagatingErrorBoundary 
  onError={(error) => { /* 传递给上层 */ }}
  fallback={<LocalFallback />}
>
  <ChildComponent />
</PropagatingErrorBoundary>

生产环境错误处理

// 生产环境专用错误边界
class ProductionErrorBoundary extends React.Component {
  state = { hasError: false };
  
  componentDidCatch(error, errorInfo) {
    this.setState({ hasError: true });
    
    // 生产环境特殊处理
    if (process.env.NODE_ENV === 'production') {
      sendProductionErrorReport(error, {
        componentStack: errorInfo.componentStack,
        userAgent: navigator.userAgent
      });
    }
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.productionFallback || (
        <div className="production-error">
          <h2>抱歉,出现了一些问题</h2>
          <p>我们的团队已收到通知,正在修复</p>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

错误边界最佳实践

分层错误处理策略

graph LR
    A[顶层边界] -->|捕获全局错误| B[应用崩溃页面]
    C[模块边界] -->|捕获模块错误| D[模块降级UI]
    E[组件边界] -->|捕获组件错误| F[组件占位符]
    G[API边界] -->|捕获数据错误| H[空状态提示]

错误恢复策略比较

策略 实现方式 适用场景
页面刷新 window.location.reload() 全局状态不可恢复
组件重置 key 属性变化 独立组件失败
状态回滚 恢复之前的状态快照 表单操作失败
备用数据 显示缓存数据 数据获取失败

错误日志规范

function logErrorBoundary(error, info) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    message: error.message,
    stack: error.stack,
    componentStack: info.componentStack,
    location: window.location.href,
    user: currentUser ? currentUser.id : 'anonymous',
    platform: navigator.platform,
    environment: process.env.NODE_ENV,
    version: process.env.REACT_APP_VERSION
  };
  
  // 发送到日志服务
  fetch('/api/logs/errors', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(logEntry)
  });
}

面试高频问题解析

Q:错误边界可以捕获哪些类型的错误?

  • 子组件渲染期间的错误
  • 生命周期方法中的错误(包括渲染阶段)
  • 构造函数中的错误
  • 整个组件树的错误(包括异步代码中的错误?不行!)

特别注意:

  • 事件处理程序中的错误不会被捕获
  • 异步代码(setTimeout, Promise)错误不会被捕获
  • 服务端渲染错误不会被捕获
  • 错误边界自身抛出的错误不会被捕获

Q:为什么错误边界必须用类组件实现?

  • 因为错误边界依赖 getDerivedStateFromError 和 componentDidCatch 生命周期方法
  • 这两个方法目前只在类组件中可用
  • React 团队表示暂时没有计划在函数组件中添加类似能力

Q:如何在函数组件中实现类似错误边界的功能?

  • 使用类组件包装:创建错误边界类组件包裹函数组件
  • 自定义Hook:使用 useErrorBoundary Hook 手动捕获错误
  • 第三方库:使用 react-error-boundary 库的 useErrorBoundary Hook
  • 错误传播:在函数组件中抛出错误让父级错误边界捕获

Q:如何测试错误边界组件?

测试策略:

// 使用 Jest 和 React Testing Library
test('捕获错误并显示降级UI', () => {
  // 创建抛出错误的组件
  const ErrorComponent = () => {
    throw new Error('测试错误');
  };
  
  const { getByText } = render(
    <ErrorBoundary fallback={<div>错误捕获</div>}>
      <ErrorComponent />
    </ErrorBoundary>
  );
  
  expect(getByText('错误捕获')).toBeInTheDocument();
});

// 测试错误日志
test('记录错误到监控系统', () => {
  const mockErrorLogger = jest.fn();
  const error = new Error('测试错误');
  
  render(
    <ErrorBoundary onError={mockErrorLogger}>
      <ErrorComponent />
    </ErrorBoundary>
  );
  
  expect(mockErrorLogger).toHaveBeenCalledWith(
    error,
    expect.objectContaining({
      componentStack: expect.any(String)
    })
  );
});

总结

  • 核心功能:
    • 捕获子组件树中的渲染错误
    • 展示降级UI替代崩溃界面
    • 记录错误信息用于分析
  • 实现关键:
    • getDerivedStateFromError 设置错误状态
    • componentDidCatch 记录错误信息
    • 类组件专属实现
  • 最佳实践:
    • 分层错误边界策略
    • 生产环境专用处理
    • 与Suspense协同使用
    • 完善的错误日志记录
  • React 18 增强:
    • 与并发渲染兼容
    • 与Suspense深度集成
    • 更精细的错误处理控制
  • 面试重点:
    • 错误边界的捕获范围
    • 类组件的必要性
    • 函数组件的替代方案
    • 测试策略与方法

4.3 代码分割与懒加载(React.lazy + Suspense)

代码分割核心概念

问题背景:传统打包痛点

graph TD
    A[单入口打包] --> B[巨大的 bundle 文件]
    B --> C[首屏加载缓慢]
    B --> D[资源浪费]
    B --> E[用户体验差]

代码分割核心价值

优化方向 实现效果 技术手段
首屏加载 减少初始加载体积 路由级分割
按需加载 使用时才加载资源 组件级懒加载
缓存优化 更细粒度缓存控制 模块分离
并行加载 利用浏览器并发能力 多 chunk 并行

基础实现方案

Webpack 动态导入

// 动态导入语法
import("./math").then(math => {
  console.log(math.add(16, 26));
});

// React 组件动态导入
const ProductModal = React.lazy(() => import('./ProductModal'));

React.lazy 基础用法

const HomePage = React.lazy(() => import('./pages/HomePage'));
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
const ContactPage = React.lazy(() => import('./pages/ContactPage'));

function App() {
  return (
    <Routes>
      <Route path="/" element={
        <Suspense fallback={<Spinner />}>
          <HomePage />
        </Suspense>
      } />
      <Route path="/about" element={
        <Suspense fallback={<Spinner />}>
          <AboutPage />
        </Suspense>
      } />
      <Route path="/contact" element={
        <Suspense fallback={<Spinner />}>
          <ContactPage />
        </Suspense>
      } />
    </Routes>
  );
}

Suspense 核心机制

function UserDashboard() {
  return (
    <div>
      <UserProfile />
      
      <Suspense fallback={<CardSkeleton />}>
        <RecentActivities />
      </Suspense>
      
      <Suspense fallback={<ChartPlaceholder />}>
        <PerformanceChart />
      </Suspense>
    </div>
  );
}

// 嵌套 Suspense 示例
function App() {
  return (
    <Suspense fallback={<FullPageSpinner />}>
      <Header />
      
      <main>
        <Suspense fallback={<ContentLoader />}>
          <Router>
            <Routes>
              {/* 路由配置 */}
            </Routes>
          </Router>
        </Suspense>
      </main>
      
      <Suspense fallback={null}>
        <LiveChatWidget />
      </Suspense>
    </Suspense>
  );
}

高级优化策略

路由级代码分割

// 路由配置文件
const routes = [
  {
    path: '/',
    element: (
      <Suspense fallback={<PageLoader />}>
        <HomePage />
      </Suspense>
    )
  },
  {
    path: '/dashboard',
    element: (
      <Suspense fallback={<DashboardSkeleton />}>
        <DashboardLayout />
      </Suspense>
    ),
    children: [
      {
        path: 'analytics',
        element: (
          <Suspense fallback={<ChartLoader />}>
            <AnalyticsPage />
          </Suspense>
        )
      },
      {
        path: 'settings',
        element: (
          <Suspense fallback={<SettingsPlaceholder />}>
            <SettingsPage />
          </Suspense>
        )
      }
    ]
  }
];

组件级懒加载

const ProductGallery = React.lazy(() => import('./ProductGallery'));
const ProductReviews = React.lazy(() => import('./ProductReviews'));
const RelatedProducts = React.lazy(() => import('./RelatedProducts'));

function ProductDetailPage() {
  const [activeTab, setActiveTab] = useState('gallery');
  
  return (
    <div>
      {/* 标签切换 */}
      <Tabs onChange={setActiveTab} />
      
      <div className="tab-content">
        {activeTab === 'gallery' && (
          <Suspense fallback={<ImageGridSkeleton />}>
            <ProductGallery />
          </Suspense>
        )}
        
        {activeTab === 'reviews' && (
          <Suspense fallback={<ReviewListSkeleton />}>
            <ProductReviews />
          </Suspense>
        )}
        
        {activeTab === 'related' && (
          <Suspense fallback={<ProductCardSkeleton count={3} />}>
            <RelatedProducts />
          </Suspense>
        )}
      </div>
    </div>
  );
}

预加载优化

// 预加载组件
function PreloadComponent({ component }) {
  useEffect(() => {
    component.preload();
  }, [component]);
  
  return null;
}

// 路由预加载
function App() {
  useEffect(() => {
    // 预加载可能访问的页面
    const preloadPages = [
      import('./pages/AboutPage'),
      import('./pages/ContactPage')
    ];
    
    // 用户空闲时预加载
    const idleCallback = requestIdleCallback(() => {
      preloadPages.forEach(preload => preload());
    });
    
    return () => cancelIdleCallback(idleCallback);
  }, []);
  
  return (
    <Router>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={
          <Suspense fallback={<Loader />}>
            <React.lazy(() => import('./pages/AboutPage')) />
          </Suspense>
        } />
        <Route path="/contact" element={
          <Suspense fallback={<Loader />}>
            <React.lazy(() => import('./pages/ContactPage')) />
          </Suspense>
        } />
      </Routes>
      
      {/* 悬停预加载 */}
      <NavLink 
        to="/about"
        onMouseEnter={() => import('./pages/AboutPage')}
      >
        关于我们
      </NavLink>
    </Router>
  );
}

性能优化进阶技巧

模块联合(微前端)

// 动态加载远程模块
const RemoteComponent = React.lazy(() =>
  import('remote_app/Component').catch(() => 
    import('./FallbackComponent')
  )
);

function App() {
  return (
    <div>
      <Suspense fallback={<RemoteLoader />}>
        <RemoteComponent />
      </Suspense>
    </div>
  );
}

// Webpack 配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      remotes: {
        remote_app: 'remote_app@http://remote-domain.com/remoteEntry.js'
      }
    })
  ]
};

资源优先级控制

// 预加载关键资源
function CriticalResourceLoader() {
  return (
    <Head>
      {/* 关键 CSS */}
      <link 
        rel="preload" 
        href="/critical.css" 
        as="style" 
        onLoad="this.rel='stylesheet'"
      />
      
      {/* 关键脚本 */}
      <link 
        rel="modulepreload" 
        href="/main.js" 
        as="script"
      />
      
      {/* 字体预加载 */}
      <link
        rel="preload"
        href="/fonts/inter.woff2"
        as="font"
        type="font/woff2"
        crossOrigin="anonymous"
      />
    </Head>
  );
}

// 延迟加载非关键资源
function LazyNonCritical() {
  useEffect(() => {
    const loadNonCritical = () => {
      import('./analytics.js');
      import('./chat-widget.js');
    };
    
    // 空闲时加载
    if ('requestIdleCallback' in window) {
      requestIdleCallback(loadNonCritical);
    } else {
      setTimeout(loadNonCritical, 5000);
    }
  }, []);
  
  return null;
}

骨架屏优化技巧

// 高级骨架屏组件
function ProductCardSkeleton({ count = 1 }) {
  return Array.from({ length: count }).map((_, index) => (
    <div key={index} className="skeleton-card">
      <div className="skeleton-image" />
      <div className="skeleton-line w-3/4" />
      <div className="skeleton-line w-1/2" />
      <div className="skeleton-line w-1/3" />
    </div>
  ));
}
// CSS 动画优化
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.skeleton-card {
  animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  background-color: #e5e7eb;
  border-radius: 0.5rem;
  overflow: hidden;
}

.skeleton-line {
  height: 1rem;
  margin-top: 0.75rem;
  background-color: #d1d5db;
  border-radius: 0.25rem;
}

常见问题与解决方案

大型模块加载优化

// 分块加载大型模块
async function loadLargeModule() {
  const [part1, part2, part3] = await Promise.all([
    import('./modulePart1'),
    import('./modulePart2'),
    import('./modulePart3')
  ]);
  
  return {
    ...part1,
    ...part2,
    ...part3
  };
}

// 渐进式加载
const LargeComponent = React.lazy(async () => {
  // 先加载核心部分
  const core = await import('./CoreComponent');
  
  // 空闲时加载增强部分
  requestIdleCallback(async () => {
    const enhancements = await import('./Enhancements');
    core.enhance(enhancements);
  });
  
  return core;
});

总结

  • 核心价值:
    • 显著减少首屏加载时间
    • 按需加载提升资源利用率
    • 改善用户体验
  • 关键技术:
    • React.lazy:组件动态导入
    • Suspense:加载状态管理
    • 动态 import():代码分割基础
  • 最佳实践:
    • 路由级分割:按页面拆分
    • 组件级懒加载:非核心功能延迟加载
    • 预加载优化:预测用户行为
    • 骨架屏技术:优化加载体验
  • React 18 增强:
    • 流式 SSR 渲染
    • 并发模式下的 Suspense
    • 更精细的加载控制
  • 企业级方案:
    • 错误边界集成
    • 微前端架构支持
    • 性能监控与分析
  • 常见问题解决:
    • 加载闪烁控制
    • 模块加载失败处理
    • 大型模块优化

4.4 Portals(渲染到 DOM 外部节点)

Portals 核心概念

解决的问题场景

graph TD
    A[父组件] --> B[overflow: hidden]
    A --> C[z-index 层级限制]
    A --> D[transform 上下文]
    B --> E[子元素显示不全]
    C --> F[模态框被遮挡]
    D --> G[fixed 定位失效]

Portals 核心能力

能力 描述 应用场景
突破层级限制 渲染到 DOM 树任意位置 模态框、弹出层
保留 React 上下文 保持事件冒泡和上下文 全局通知
样式隔离 避免父容器样式影响 工具提示
无障碍支持 管理焦点和键盘事件 无障碍对话框

基础用法与API

ReactDOM.createPortal 基础

import ReactDOM from 'react-dom';

function Modal({ children, isOpen }) {
  if (!isOpen) return null;
  
  // 创建 Portal 渲染到 body 外的独立容器
  return ReactDOM.createPortal(
    <div className="modal">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    document.getElementById('modal-root') // 外部容器
  );
}

// 在 public/index.html 中添加容器
<body>
  <div id="root"></div>
  <div id="modal-root"></div> <!-- Portal 目标容器 -->
</body>

动态容器管理

function SmartPortal({ children, containerId = 'portal-root' }) {
  const [container, setContainer] = useState(null);
  
  useEffect(() => {
    // 查找或创建容器
    let portalContainer = document.getElementById(containerId);
    
    if (!portalContainer) {
      portalContainer = document.createElement('div');
      portalContainer.id = containerId;
      document.body.appendChild(portalContainer);
    }
    
    setContainer(portalContainer);
    
    return () => {
      // 清理容器
      if (portalContainer) {
        document.body.removeChild(portalContainer);
      }
    };
  }, [containerId]);
  
  if (!container) return null;
  
  return ReactDOM.createPortal(children, container);
}

// 使用示例
<SmartPortal>
  <Notification message="操作成功!" />
</SmartPortal>

核心应用场景

模态对话框实现

function ModalDialog({ title, children, onClose }) {
  // 阻止背景滚动
  useEffect(() => {
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = '';
    };
  }, []);
  
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div 
        className="modal-dialog" 
        onClick={e => e.stopPropagation()}
      >
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose} aria-label="关闭">
            &times;
          </button>
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  );
}

// 使用 Portal 的模态框组件
export default function Modal({ isOpen, ...props }) {
  if (!isOpen) return null;
  
  return ReactDOM.createPortal(
    <ModalDialog {...props} />,
    document.getElementById('modal-root')
  );
}

全局通知系统

const NotificationContext = React.createContext();

function NotificationProvider({ children }) {
  const [notifications, setNotifications] = useState([]);
  
  const addNotification = (message, type = 'info') => {
    const id = Date.now();
    setNotifications(prev => [...prev, { id, message, type }]);
    
    // 自动关闭
    setTimeout(() => {
      removeNotification(id);
    }, 5000);
  };
  
  const removeNotification = (id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  };
  
  return (
    <NotificationContext.Provider value={{ addNotification }}>
      {children}
      
      {/* 使用 Portal 渲染到独立区域 */}
      <SmartPortal containerId="notification-portal">
        <div className="notification-container">
          {notifications.map(notification => (
            <NotificationItem 
              key={notification.id}
              notification={notification}
              onClose={() => removeNotification(notification.id)}
            />
          ))}
        </div>
      </SmartPortal>
    </NotificationContext.Provider>
  );
}

// 通知项组件
function NotificationItem({ notification, onClose }) {
  return (
    <div className={`notification ${notification.type}`}>
      <p>{notification.message}</p>
      <button onClick={onClose} aria-label="关闭通知">
        &times;
      </button>
    </div>
  );
}

工具提示与悬浮框

function Tooltip({ children, content }) {
  const [isVisible, setIsVisible] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);
  
  const updatePosition = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPosition({
        top: rect.bottom + window.scrollY,
        left: rect.left + rect.width / 2
      });
    }
  };
  
  useLayoutEffect(() => {
    if (isVisible) {
      updatePosition();
      window.addEventListener('resize', updatePosition);
      return () => window.removeEventListener('resize', updatePosition);
    }
  }, [isVisible]);
  
  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
        onFocus={() => setIsVisible(true)}
        onBlur={() => setIsVisible(false)}
        aria-describedby="tooltip-content"
      >
        {children}
      </span>
      
      {/* 使用 Portal 避免被父容器裁剪 */}
      {isVisible && (
        <SmartPortal>
          <div 
            id="tooltip-content"
            className="tooltip"
            role="tooltip"
            style={{
              position: 'absolute',
              top: `${position.top}px`,
              left: `${position.left}px`,
              transform: 'translateX(-50%)'
            }}
          >
            {content}
          </div>
        </SmartPortal>
      )}
    </>
  );
}

高级特性与模式

事件冒泡处理

function EventDemo() {
  const handlePortalClick = () => {
    console.log('Portal 内容被点击');
  };
  
  const handleParentClick = () => {
    console.log('父组件被点击');
  };
  
  return (
    <div onClick={handleParentClick} className="parent">
      <h2>事件冒泡测试</h2>
      
      {/* Portal 内容会冒泡到 React 树中的父组件 */}
      {ReactDOM.createPortal(
        <button onClick={handlePortalClick}>
          点击我(Portal 按钮)
        </button>,
        document.getElementById('portal-root')
      )}
      
      <p>
        点击 Portal 按钮会触发两个日志:<br />
        1. "Portal 内容被点击"<br />
        2. "父组件被点击"
      </p>
    </div>
  );
}

上下文传递

const ThemeContext = React.createContext('light');

function ThemedPortalDemo() {
  return (
    <ThemeContext.Provider value="dark">
      <div className="app">
        <Header />
        
        {/* Portal 内容能访问 React 树中的上下文 */}
        <SmartPortal>
          <ThemeContext.Consumer>
            {theme => (
              <div className={`theme-box ${theme}`}>
                当前主题: {theme}
              </div>
            )}
          </ThemeContext.Consumer>
        </SmartPortal>
      </div>
    </ThemeContext.Provider>
  );
}

无障碍对话框实现

function AccessibleModal({ isOpen, title, children, onClose }) {
  const modalRef = useRef(null);
  
  // 关闭时返回焦点到触发元素
  useEffect(() => {
    const triggerElement = document.activeElement;
    
    if (isOpen && modalRef.current) {
      modalRef.current.focus();
    }
    
    return () => {
      if (triggerElement) {
        triggerElement.focus();
      }
    };
  }, [isOpen]);
  
  // ESC 键关闭
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (e.key === 'Escape') {
        onClose();
      }
    };
    
    if (isOpen) {
      document.addEventListener('keydown', handleKeyDown);
    }
    
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [isOpen, onClose]);
  
  if (!isOpen) return null;
  
  return ReactDOM.createPortal(
    <div 
      className="modal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      ref={modalRef}
      tabIndex={-1}
    >
      <div className="modal-content">
        <h2 id="modal-title">{title}</h2>
        <div>{children}</div>
        <button onClick={onClose} aria-label="关闭对话框">
          关闭
        </button>
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

企业级最佳实践

微前端集成

function MicroFrontendPortal({ appName, componentName }) {
  const [Component, setComponent] = useState(null);
  
  useEffect(() => {
    // 动态加载远程模块
    const loadRemoteComponent = async () => {
      try {
        const remoteModule = await window[appName].get(componentName);
        setComponent(() => remoteModule);
      } catch (error) {
        console.error(`加载 ${appName}/${componentName} 失败:`, error);
      }
    };
    
    loadRemoteComponent();
  }, [appName, componentName]);
  
  if (!Component) return <div>加载中...</div>;
  
  return (
    <SmartPortal containerId={`${appName}-portal`}>
      <Component />
    </SmartPortal>
  );
}

// 使用示例
<MicroFrontendPortal 
  appName="billingApp" 
  componentName="PaymentWidget" 
/>

面试高频问题解析

Q:Portals 和普通渲染有什么区别?

特性 普通渲染 Portal
DOM 位置 父组件内 任意 DOM 节点
事件冒泡 遵循 DOM 结构 遵循 React 树结构
上下文访问 正常访问 正常访问
样式继承 受父容器影响 可避免父容器影响
应用场景 常规 UI 模态框、通知等

Q:为什么 Portal 的事件冒泡到 React 父组件?

  • Portal 的特殊之处在于它在 DOM 树中的位置是独立的
  • 但 React 的事件系统是合成的,不依赖实际 DOM 结构
  • 事件冒泡基于 React 组件树结构,而非实际 DOM 结构
  • 因此 Portal 中的事件会冒泡到 React 树中的父组件

Q:如何阻止 Portal 的事件冒泡?

解决方案:

function PortalWithStopPropagation() {
  const handlePortalClick = (e) => {
    e.stopPropagation(); // 阻止事件冒泡
    console.log('Portal 点击');
  };
  
  return ReactDOM.createPortal(
    <div onClick={handlePortalClick}>
      <button>点击我</button>
    </div>,
    document.getElementById('portal-root')
  );
}

Q:Portals 对无障碍有什么影响?

最佳实践:

  • 焦点管理:打开 Portal 时移动焦点到内容,关闭时返回焦点
  • ARIA 属性:使用 role=”dialog” 和 aria-modal=”true”
  • 键盘导航:实现 ESC 关闭和 Tab 键陷阱
  • 屏幕阅读器:使用 aria-labelledby 关联标题
  • 背景遮蔽:添加 aria-hidden=”true” 到背景内容

总结

  • 核心价值:
    • 突破 CSS 层级限制(overflow, z-index, transform)
    • 保持 React 上下文和事件系统
    • 实现全局 UI 元素(模态框、通知、工具提示)
  • 关键技术:
    • ReactDOM.createPortal() API
    • 动态容器管理
    • 事件冒泡处理
    • 无障碍支持
  • 最佳实践:
    • 模态框实现(包含焦点管理)
    • 全局通知系统
    • 服务端渲染兼容
    • 微前端集成
  • React 18 增强:
    • 新的 Root API 支持
    • 并发渲染兼容
    • 更精细的控制能力
  • 企业级应用:
    • 安全沙箱模式
    • 性能优化策略
    • 调试与监控
  • 面试重点:
    • Portals 与普通渲染的区别
    • 事件冒泡机制
    • 无障碍实现
    • 服务端渲染处理

4.5 高阶组件(HOC)与 Render Props

组件复用的核心挑战

传统组件复用的问题

graph TD
    A[组件复用需求] --> B[直接继承]
    A --> C[组合模式]
    B --> D[紧耦合]
    B --> E[灵活性差]
    C --> F[多层嵌套]
    C --> G[props 透传问题]

解决方案对比

模式 核心思想 适用场景 优点 缺点
高阶组件 (HOC) 函数接受组件返回新组件 横切关注点 逻辑复用强 嵌套过深
Render Props 组件通过 prop 渲染内容 动态组合 灵活直观 回调地狱
自定义 Hooks 函数封装可复用逻辑 功能复用 简洁高效 仅函数组件

高阶组件(HOC)深度解析

HOC 核心概念

定义:高阶组件是一个函数,接受一个组件并返回一个新的增强组件。

const EnhancedComponent = withEnhancement(WrappedComponent);

HOC 实现模式

  • 属性代理(Props Proxy)
    function withDataFetching(WrappedComponent, fetchUrl) {
      return class extends React.Component {
        state = { data: null, loading: true, error: null };
        
        componentDidMount() {
          fetch(fetchUrl)
            .then(res => res.json())
            .then(data => this.setState({ data, loading: false }))
            .catch(error => this.setState({ error, loading: false }));
        }
        
        render() {
          const { data, loading, error } = this.state;
          
          // 注入额外 props
          return (
            <WrappedComponent 
              {...this.props} 
              data={data} 
              loading={loading} 
              error={error} 
            />
          );
        }
      };
    }
    
    // 使用
    const UserProfileWithData = withDataFetching(UserProfile, '/api/user/123');
  • 反向继承(Inheritance Inversion)
    function withLogging(WrappedComponent) {
      return class extends WrappedComponent {
        componentDidMount() {
          console.log('Component mounted:', WrappedComponent.name);
          super.componentDidMount?.();
        }
        
        componentWillUnmount() {
          console.log('Component unmounted:', WrappedComponent.name);
          super.componentWillUnmount?.();
        }
        
        render() {
          // 可操作渲染输出
          if (this.props.hidden) {
            return null;
          }
          
          return super.render();
        }
      };
    }
    
    // 使用
    const EnhancedComponent = withLogging(MyComponent);

HOC 链式组合

// 组合多个 HOC
const enhance = compose(
  withRouter,
  withStyles(styles),
  withDataFetching('/api/data'),
  withErrorBoundary
);

const SuperComponent = enhance(BaseComponent);

// 自定义 compose 函数
function compose(...hocs) {
  return Component => 
    hocs.reduceRight((acc, hoc) => hoc(acc), Component);
}

HOC 最佳实践

  • 显示名称设置
    function withDisplayName(WrappedComponent) {
      const WithDisplayName = props => <WrappedComponent {...props} />;
      
      // 设置 displayName 便于调试
      const displayName = 
        WrappedComponent.displayName || WrappedComponent.name || 'Component';
      
      WithDisplayName.displayName = `WithDisplayName(${displayName})`;
      
      return WithDisplayName;
    }
  • 静态方法复制
    function copyStaticMethods(WrappedComponent, EnhancedComponent) {
      // 复制静态方法
      Object.keys(WrappedComponent).forEach(key => {
        if (typeof WrappedComponent[key] === 'function') {
          EnhancedComponent[key] = WrappedComponent[key];
        }
      });
      
      // 复制静态属性
      EnhancedComponent.staticProperty = WrappedComponent.staticProperty;
      
      return EnhancedComponent;
    }
  • Refs 转发
    function withRefForwarding(WrappedComponent) {
      class WithRefForwarding extends React.Component {
        render() {
          const { forwardedRef, ...rest } = this.props;
          
          return (
            <WrappedComponent 
              ref={forwardedRef} 
              {...rest} 
            />
          );
        }
      }
      
      // 使用 forwardRef 转发 ref
      return React.forwardRef((props, ref) => (
        <WithRefForwarding {...props} forwardedRef={ref} />
      ));
    }

Render Props 深度解析

Render Props 核心概念

定义:组件通过一个函数 prop(通常命名为 render 或 children)来动态决定渲染内容。

<DataProvider render={data => (
  <div>{data}</div>
)} />

基础实现模式

  • 标准 Render Prop
    class MouseTracker extends React.Component {
      state = { x: 0, y: 0 };
      
      handleMouseMove = event => {
        this.setState({
          x: event.clientX,
          y: event.clientY
        });
      };
      
      render() {
        return (
          <div onMouseMove={this.handleMouseMove}>
            {this.props.render(this.state)}
          </div>
        );
      }
    }
    
    // 使用
    <MouseTracker render={({ x, y }) => (
      <p>鼠标位置: ({x}, {y})</p>
    )} />
  • Children as Function
    function Toggle({ children }) {
      const [on, setOn] = useState(false);
      
      const toggle = () => setOn(!on);
      
      return children({
        on,
        toggle
      });
    }
    
    // 使用
    <Toggle>
      {({ on, toggle }) => (
        <div>
          <button onClick={toggle}>
            {on ? 'ON' : 'OFF'}
          </button>
          {on && <Modal content="开启状态" />}
        </div>
      )}
    </Toggle>

高级 Render Props 模式

  • 组合多个 Render Props
    function CombinedProvider({ data, theme, children }) {
      return (
        <DataProvider data={data}>
          {dataProps => (
            <ThemeProvider theme={theme}>
              {themeProps => children({ ...dataProps, ...themeProps })}
            </ThemeProvider>
          )}
        </DataProvider>
      );
    }
    
    // 使用
    <CombinedProvider data={apiData} theme="dark">
      {({ data, loading, theme }) => (
        <div className={`app ${theme}`}>
          {loading ? <Spinner /> : <Content data={data} />}
        </div>
      )}
    </CombinedProvider>
  • Render Props + Hooks
    function useCounter(initialCount = 0) {
      const [count, setCount] = useState(initialCount);
      
      const increment = () => setCount(c => c + 1);
      const decrement = () => setCount(c => c - 1);
      
      return { count, increment, decrement };
    }
    
    function Counter({ children }) {
      const counter = useCounter();
      return children(counter);
    }
    
    // 使用
    <Counter>
      {({ count, increment, decrement }) => (
        <div>
          <button onClick={decrement}>-</button>
          <span>{count}</span>
          <button onClick={increment}>+</button>
        </div>
      )}
    </Counter>

HOC 与 Render Props 对比

技术对比

graph LR
    A[HOC] --> B[属性代理]
    A --> C[反向继承]
    D[Render Props] --> E[函数作为子组件]
    D --> F[Render Prop]
    
    B --> G[注入Props]
    C --> H[操作组件]
    E --> I[直接组合]
    F --> I

详细对比分析

特性 高阶组件 (HOC) Render Props
复用方式 组件作为参数返回增强组件 函数作为prop渲染内容
组件关系 包装关系 包含关系
调试难度 较难(多层包装) 较易(直接显示)
静态类型 类型推导复杂 类型推导简单
灵活性 中等
性能影响 可能引起额外渲染 可能引起额外渲染
学习曲线 较陡峭 较平缓
适用场景 横切关注点 动态组合

企业级最佳实践

权限控制

function withAuthorization(requiredRoles) {
  return WrappedComponent => {
    const AuthorizedComponent = props => {
      const { user } = useAuth();
      
      if (!user || !requiredRoles.some(role => user.roles.includes(role))) {
        return <ForbiddenPage />;
      }
      
      return <WrappedComponent {...props} />;
    };
    
    return AuthorizedComponent;
  };
}

// 使用
const AdminDashboard = withAuthorization(['admin', 'superadmin'])(Dashboard);

现代替代方案:自定义 Hooks

重构 HOC 为 Hook

// 原 HOC
function withWindowSize(WrappedComponent) {
  return class extends React.Component {
    state = { width: window.innerWidth, height: window.innerHeight };
    
    handleResize = () => {
      this.setState({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    componentDidMount() {
      window.addEventListener('resize', this.handleResize);
    }
    
    componentWillUnmount() {
      window.removeEventListener('resize', this.handleResize);
    }
    
    render() {
      return <WrappedComponent {...this.props} windowSize={this.state} />;
    }
  };
}

// 自定义 Hook
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return size;
}

// 使用
function ResponsiveComponent() {
  const { width } = useWindowSize();
  return <div>窗口宽度: {width}px</div>;
}

重构 Render Props 为 Hook

// 原 Render Prop 组件
class DataFetcher extends React.Component {
  state = { data: null, loading: true, error: null };
  
  componentDidMount() {
    fetch(this.props.url)
      .then(res => res.json())
      .then(data => this.setState({ data, loading: false }))
      .catch(error => this.setState({ error, loading: false }));
  }
  
  render() {
    return this.props.children(this.state);
  }
}

// 自定义 Hook
function useDataFetcher(url) {
  const [state, setState] = useState({ 
    data: null, 
    loading: true, 
    error: null 
  });
  
  useEffect(() => {
    setState(prev => ({ ...prev, loading: true }));
    
    fetch(url)
      .then(res => res.json())
      .then(data => setState({ data, loading: false, error: null }))
      .catch(error => setState({ data: null, loading: false, error }));
  }, [url]);
  
  return state;
}

// 使用
function UserProfile({ userId }) {
  const { data, loading } = useDataFetcher(`/api/users/${userId}`);
  
  if (loading) return <Spinner />;
  return <ProfileCard user={data} />;
}

设计模式选择指南

模式选择决策

graph TD
    A[需要逻辑复用] --> B{需要访问组件内部?}
    B -->|是| C[Render Props]
    B -->|否| D{需要包装组件?}
    D -->|是| E[HOC]
    D -->|否| F[自定义 Hook]
    C --> G{功能简单?}
    G -->|是| H[Render Props]
    G -->|否| I[自定义 Hook]

场景化建议

  • 跨组件共享逻辑:HOC(如 Redux connect)
  • 动态组合UI:Render Props(如 Context.Consumer)
  • 功能复用:自定义 Hooks(如 useForm)
  • 类组件扩展:HOC(如 withRouter)
  • 函数组件扩展:自定义 Hooks(如 useEffect)

面试高频问题解析

Q:HOC 和 Render Props 的主要区别是什么?

  • HOC:组件工厂模式,通过包装组件增强功能,可能引入多层嵌套
  • Render Props:回调模式,通过函数 prop 共享状态,更直接但可能嵌套回调
  • 关键区别:HOC 创建新组件,Render Props 动态组合组件

Q:使用 HOC 时如何避免命名冲突?

function withEnhancedProps(WrappedComponent) {
  return class extends React.Component {
    render() {
      // 使用命名空间避免冲突
      const enhancedProps = {
        hoc: {
          data: 'hoc data',
          method: () => console.log('HOC method')
        },
        ...this.props
      };
      
      return <WrappedComponent {...enhancedProps} />;
    }
  };
}

Q:Render Props 会导致性能问题吗?如何优化?

优化策略:

  • 避免内联函数
    // 不推荐:每次渲染创建新函数
    <DataProvider render={data => <Child data={data} />} />
    
    // 推荐:使用实例方法
    renderData = data => <Child data={data} />;
    
    <DataProvider render={this.renderData} />
  • 使用 PureComponent/memo
    class OptimizedProvider extends React.PureComponent {
      // 仅当 props 变化时重新渲染
    }
  • 分离渲染职责
    function SplitProvider({ children }) {
      const data = useData();
      const ui = useUI();
      
      return (
        <DataContext.Provider value={data}>
          <UIContext.Provider value={ui}>
            {children}
          </UIContext.Provider>
        </DataContext.Provider>
      );
    }

Q:何时应该选择自定义 Hooks 替代 HOC 或 Render Props?

  • 选择自定义 Hooks 当:
    • 复用逻辑不依赖 React 生命周期
    • 需要在多个组件中使用相同逻辑
    • 项目主要使用函数组件
    • 希望减少组件嵌套层级
  • 保留 HOC/Render Props 当:
    • 维护类组件代码库
    • 需要操作组件实例(如 refs)
    • 需要访问组件的生命周期方法
    • 需要包装组件(如添加错误边界)

总结

  • 高阶组件 (HOC):
    • 本质:组件工厂函数(输入组件 → 输出增强组件)
    • 模式:属性代理、反向继承
    • 适用:横切关注点、类组件扩展
    • 注意:ref 传递、命名冲突、静态方法
  • Render Props:
    • 本质:通过函数 prop 共享状态
    • 模式:render prop 或 children as function
    • 适用:动态组合、复杂状态共享
    • 注意:回调嵌套、性能优化
  • 现代演进:
    • 自定义 Hooks 成为函数组件首选
    • HOC 和 Render Props 仍适合特定场景
    • 三种模式可混合使用
  • 设计原则:
    • 单一职责原则
    • 开闭原则(对扩展开放,对修改关闭)
    • 组合优于继承
  • 企业实践:
    • 复杂状态管理
    • 权限控制
    • 性能优化
    • 类型安全(TypeScript)
  • 面试重点:
    • HOC 与 Render Props 区别
    • 命名冲突解决方案
    • 性能优化策略
    • 模式选择决策

第五章:React 状态管理

5.1 状态提升与单向数据流

核心概念全景图

graph TD
  A[状态管理方案] --> B[组件内部状态]
  A --> C[状态提升]
  A --> D[全局状态管理]
  C --> E[父子组件通信]
  C --> F[兄弟组件通信]

状态提升(Lifting State Up)深度解析

基本定义
将多个组件需要共享的状态提升到它们最近的公共父组件中管理,通过 props 向下传递数据,通过回调函数向上传递状态变更

解决什么问题

// 问题场景:两个独立组件需要同步状态
function TemperatureInputA() {
  const [temp, setTemp] = useState(0); // 独立状态
  // ...
}

function TemperatureInputB() {
  const [temp, setTemp] = useState(0); // 独立状态
  // ...
}

// 无法保持两个输入框温度值同步

解决方案

function Parent() {
  // 1. 状态提升到公共父组件
  const [temperature, setTemperature] = useState(0);
  
  // 2. 通过props传递状态
  return (
    <div>
      <TemperatureInput 
        value={temperature} 
        onChange={setTemperature} // 3. 传递回调函数
      />
      <TemperatureDisplay 
        value={temperature} 
      />
    </div>
  );
}

// 子组件成为受控组件
function TemperatureInput({ value, onChange }) {
  return (
    <input 
      type="number"
      value={value}
      onChange={(e) => onChange(Number(e.target.value))}
    />
  );
}

单向数据流(Unidirectional Data Flow)

核心原则

graph LR
  A[State] --> B[UI]
  B --> C[Actions]
  C --> D[State Changes]
  D --> A

React 中的实现

graph TD
  P[父组件State] -->|Props| C1[子组件A]
  P -->|Props| C2[子组件B]
  C1 -->|回调函数| P
  C2 -->|回调函数| P

与双向绑定的对比

特性 单向数据流 双向绑定
数据方向 父→子 父⇄子
状态管理 集中管理 分散管理
数据可预测性
调试难度 相对容易 相对复杂
典型框架 React Vue/Angular

状态提升的局限性

  • 组件树层级过深时(超过3层)
  • 非直系组件需要通信(如堂兄弟组件)
  • 跨路由组件状态共享

现代状态管理方案基础

Flux 架构思想

graph LR
  A[Action] --> B[Dispatcher]
  B --> C[Store]
  C --> D[View]
  D --> A

状态提升 vs Redux

特性 状态提升 Redux
状态存储 组件 state 独立 store
状态更新 setState dispatch(action)
更新逻辑 在组件内 reducer 纯函数
状态共享范围 父组件及子组件 全局可访问
中间件支持 支持异步操作

最佳实践总结

  1. 最小状态原则:只提升必要共享的状态
  2. 保持状态局部:非共享状态保持在组件内部
  3. 避免过度提升:超过3层传递考虑 Context
  4. 数据不可变:状态更新时创建新对象
    // ❌ 错误
    formData.items.push(newItem);
    setFormData(formData);
    
    // ✅ 正确
    setFormData(prev => ({
      ...prev,
      items: [...prev.items, newItem]
    }));
  5. 回调函数命名:统一使用 on{Event} 格式
    <Button onSave={handleSave} />

5.2 Redux 核心概念(Store、Action、Reducer)

Redux 三大核心概念全景图

graph LR
  A[Action] -->|描述事件| B[Reducer]
  B -->|处理事件| C[Store]
  C -->|提供状态| D[View]
  D -->|触发| A

Action:状态变化的描述

本质与结构

// 基本结构
const action = {
  type: 'ADD_TODO',      // 必需:动作类型
  payload: 'Learn Redux' // 可选:携带数据
}

// Flux Standard Action (FSA) 规范
{
  type: 'FETCH_USER_SUCCESS',
  payload: { id: 1, name: 'John' }, // 成功数据
  error: false,                     // 是否错误
  meta: { timestamp: Date.now() }    // 元信息
}

Action 创建函数(Action Creators)

// 同步 Action
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    payload: text
  }
}

// 异步 Action (需中间件支持)
function fetchUser(id) {
  return async dispatch => {
    dispatch({ type: 'FETCH_USER_REQUEST' });
    try {
      const user = await api.getUser(id);
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
    } catch (error) {
      dispatch({ type: 'FETCH_USER_FAILURE', error });
    }
  }
}

Reducer:状态转换的纯函数

核心特征

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      // ✅ 正确:返回新对象
      return [...state, { text: action.payload, completed: false }];
    
    case 'TOGGLE_TODO':
      // ✅ 正确:使用map创建新数组
      return state.map((todo, index) => 
        index === action.index 
          ? { ...todo, completed: !todo.completed }
          : todo
      );
      
    default:
      // ⚠️ 必须:返回当前state
      return state;
  }
}

不可变更新模式

操作 错误方式 正确方式
添加项 state.push(item) […state, item]
删除项 state.splice(index, 1) state.filter((_, i) => i !== index)
更新对象属性 state[prop] = value { …state, [prop]: value }
嵌套对象更新 state.a.b = value { …state, a: { …state.a, b: value } }

Reducer 拆分与组合

// 根Reducer
function rootReducer(state = {}, action) {
  return {
    todos: todosReducer(state.todos, action),
    visibilityFilter: filterReducer(state.visibilityFilter, action)
  }
}

// 使用 combineReducers 简化
import { combineReducers } from 'redux';
const rootReducer = combineReducers({
  todos: todosReducer,
  visibilityFilter: filterReducer
});

Store:应用状态的容器

核心职责

import { createStore } from 'redux';

// 创建Store
const store = createStore(rootReducer, initialState);

// 主要API
store.dispatch(action);  // 触发状态更新
store.getState();        // 获取当前状态
store.subscribe(listener); // 监听状态变化

// 使用示例
store.subscribe(() => {
  console.log('新状态:', store.getState());
});

store.dispatch(addTodo('Learn Redux'));

Store 工作流程

sequenceDiagram
  participant View as 视图
  participant Store as Store
  participant Reducer as Reducer
  
  View->>Store: dispatch(action)
  Store->>Reducer: 当前state + action
  Reducer->>Reducer: 计算新state
  Reducer->>Store: 返回新state
  Store->>View: 通知订阅者更新

Redux 三大原则

  1. 单一数据源 (Single Source of Truth)
    // 整个应用只有一个Store
    console.log(store.getState());
    /* 输出:
    {
      todos: [],
      visibilityFilter: 'SHOW_ALL'
    }
    */
  2. 状态只读 (State is Read-Only)
    // ❌ 禁止直接修改
    store.getState().todos.push('Illegal mutation!');
    
    // ✅ 唯一修改方式:dispatch(action)
    store.dispatch({ type: 'ADD_TODO', text: 'Legal update' });
  3. 纯函数修改 (Changes with Pure Functions)
    // Reducer必须是纯函数
    function impureReducer(state = 0, action) {
      // ❌ 错误:包含副作用
      localStorage.setItem('state', state); 
      
      // ❌ 错误:不纯(依赖外部变量)
      return state + Math.random(); 
    }

Redux 数据流完整周期

sequenceDiagram
  participant View as 视图
  participant Action as Action
  participant Middleware as 中间件
  participant Store as Store
  participant Reducer as Reducer
  
  View->>Action: 用户交互触发
  Action->>Middleware: 处理异步/副作用
  Middleware->>Store: dispatch(action)
  Store->>Reducer: (currentState, action)
  Reducer->>Store: 返回newState
  Store->>View: 通知订阅者
  View->>View: 使用新状态重渲染

高频面试题与破解方案

Q:为什么Redux要求Reducer是纯函数?

  1. 可预测性:相同输入必定得到相同输出
  2. 时间旅行:支持状态快照和回滚
  3. 易于测试:无需模拟环境,直接测试输入输出
  4. 性能优化:可安全进行浅比较

Q:Redux如何实现时间旅行调试?

// 核心原理
function createStore(reducer) {
  let state = initialState;
  const listeners = [];
  const history = []; // ⭐ 历史状态记录
  
  return {
    dispatch: (action) => {
      history.push(state); // 保存当前状态
      state = reducer(state, action); // 计算新状态
      listeners.forEach(listener => listener());
    },
    getState: () => state,
    undo: () => {
      if (history.length > 0) {
        state = history.pop(); // 恢复历史状态
        listeners.forEach(listener => listener());
      }
    }
  }
}

// 实际使用redux-undo库
import undoable from 'redux-undo';
const store = createStore(undoable(rootReducer));

Q:Redux与Flux架构有何区别?

特性 Redux Flux
Store数量 单一Store 多个Store
Dispatcher 无独立Dispatcher 有中央Dispatcher
状态更新 纯函数Reducer Store内方法更新
样板代码 相对较少 相对较多
时间旅行 原生支持 需要额外实现

Redux 在函数组件中的使用

基础连接方式

import { Provider } from 'react-redux';

// 根组件包裹Provider
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

// 子组件连接
import { connect } from 'react-redux';

function TodoList({ todos, dispatch }) {
  // ...
}

export default connect(
  state => ({ todos: state.todos }), // mapStateToProps
  dispatch => ({ dispatch })         // mapDispatchToProps
)(TodoList);

Hooks API(推荐)

import { useSelector, useDispatch } from 'react-redux';

function TodoList() {
  // 获取状态
  const todos = useSelector(state => state.todos);
  
  // 获取dispatch函数
  const dispatch = useDispatch();
  
  const addTodo = text => dispatch({ type: 'ADD_TODO', text });
  
  return (
    <ul>
      {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
    </ul>
  );
}

Redux 中间件机制

中间件原理

// 中间件签名
const middleware = store => next => action => {
  // 处理前逻辑
  console.log('dispatching', action);
  
  // 调用下一个中间件
  const result = next(action);
  
  // 处理后逻辑
  console.log('next state', store.getState());
  return result;
};

// 创建支持中间件的store
import { applyMiddleware, createStore } from 'redux';
const store = createStore(
  rootReducer,
  applyMiddleware(middleware1, middleware2)
);

常用中间件

中间件 解决的核心问题 典型使用场景
redux-thunk 异步Action处理 简单API请求
redux-saga 复杂异步流程管理 多步骤异步、竞态处理
redux-observable 响应式编程 复杂事件流处理
redux-logger 状态变更日志 开发环境调试

Redux 最佳实践

项目结构组织

src/
  ├── store/           # Redux相关
  │   ├── actions/     # Action创建函数
  │   ├── reducers/    # Reducer函数
  │   ├── sagas/       # Saga中间件
  │   └── store.js     # Store配置
  │
  ├── components/      # 展示组件
  └── containers/      # 容器组件

命名规范

// Action类型:<名词>_<动词>
export const USER_FETCH_REQUESTED = 'USER_FETCH_REQUESTED';
export const TODOS_ADD = 'TODOS_ADD';

// Action创建函数:动词+名词
export function fetchUser(id) { ... }
export function addTodo(text) { ... }

状态设计原则

  1. 扁平化结构:避免嵌套过深
  2. 按业务域拆分:不同功能独立状态分支
  3. ID驱动:通过ID关联数据

总结:Redux 核心概念关系图

graph TD
  A[Action] -->|描述| B[Reducer]
  B -->|生成| C[Store]
  C -->|提供| D[View]
  D -->|触发| A
  E[Middleware] -->|拦截| A
  F[Selector] -->|获取| C
  G[React-Redux] -->|连接| D

5.3 Redux 中间件(redux-thunk、redux-saga)

Redux 中间件核心原理

中间件在数据流中的位置

graph LR
  A[View] -->|dispatch| B[Middleware]
  B -->|"next(action)"| C[Reducer]
  C --> D[Store]
  D -->|newState| A

中间件签名与执行链

// 中间件结构(柯里化函数)
const middleware = store => next => action => {
  // 前置处理
  console.log('dispatching:', action);
  
  // 传递 action 给下一个中间件或 reducer
  const result = next(action);
  
  // 后置处理
  console.log('next state:', store.getState());
  return result;
};

// 中间件执行顺序
applyMiddleware(mid1, mid2, mid3)
// 实际调用顺序:
// mid1(store) -> mid2(store) -> mid3(store) -> reducer

redux-thunk:处理简单异步

核心概念

允许 action 创建函数返回函数(而不仅是对象),该函数接收 dispatch 和 getState 作为参数

使用示例

// 同步 action
const increment = () => ({ type: 'INCREMENT' });

// 异步 action (thunk)
const fetchUser = (userId) => async (dispatch, getState) => {
  dispatch({ type: 'USER_REQUEST' });
  
  try {
    const user = await api.getUser(userId);
    dispatch({ type: 'USER_SUCCESS', payload: user });
    
    // 可访问当前状态
    const { settings } = getState();
    if (settings.notify) showNotification('用户加载成功');
    
  } catch (error) {
    dispatch({ type: 'USER_FAILURE', error });
  }
};

// 组件中触发
dispatch(fetchUser(123));

适用场景

  • API 请求
  • 简单异步操作
  • 需要访问当前状态的异步逻辑

优点与局限

优点 局限
学习成本低 复杂异步流程难管理
代码简洁 测试相对复杂
无需额外库 不支持高级异步模式

redux-saga:处理复杂异步

核心概念

graph TD
  A[View] -->|dispatch| B[Action]
  B --> C[Watcher Saga]
  C --> D[Worker Saga]
  D --> E[API Call]
  E --> F[Action to Store]

关键特性

  • 基于 ES6 Generator 函数
  • 集中管理副作用(side effects)
  • 提供丰富异步控制原语(takeEvery, takeLatest 等)
  • 支持竞态条件处理、并行任务等复杂场景

使用示例

import { call, put, takeEvery } from 'redux-saga/effects';

// Worker Saga
function* fetchUser(action) {
  try {
    const user = yield call(api.getUser, action.payload);
    yield put({ type: 'USER_SUCCESS', payload: user });
  } catch (error) {
    yield put({ type: 'USER_FAILURE', error });
  }
}

// Watcher Saga
function* watchUserRequests() {
  yield takeEvery('USER_REQUEST', fetchUser);
}

// 根Saga
export default function* rootSaga() {
  yield all([
    watchUserRequests(),
    // 其他 watcher
  ]);
}

常用 Effect 原语

Effect 作用 示例
call 调用异步函数 yield call(api.fetch, url)
put 发起 action yield put({ type: ‘SUCCESS’ })
take 等待指定 action yield take(‘ACTION’)
takeEvery 监听每个 action yield takeEvery(‘ACTION’, saga)
takeLatest 只响应最新的 action yield takeLatest(‘ACTION’, saga)
fork 非阻塞调用 saga yield fork(otherSaga)
all 并行运行多个 Effect yield all([call(a), call(b)])
race 竞态执行 yield race({ task: call(fn), cancel: take(‘CANCEL’) })

高频面试题与破解方案

Q:为什么需要中间件处理异步?

  1. Redux 自身限制:reducer 必须是纯函数,不能包含副作用
  2. 可预测性:集中管理副作用,避免分散在组件中
  3. 可维护性:统一处理错误、日志、重试等通用逻辑
  4. 高级功能:实现请求取消、竞态处理等复杂场景

Q:如何选择 thunk 还是 saga?

graph TD
  A[项目需求] --> B{异步复杂度}
  B -->|简单API请求| C[redux-thunk]
  B -->|复杂流程/竞态| D[redux-saga]
  A --> E{团队熟悉度}
  E -->|熟悉Generator| D
  E -->|新手团队| C
  A --> F{包大小敏感}
  F -->|是| C
  F -->|否| D

5.4 React-Redux(Provider、useSelector、useDispatch)

React-Redux 核心架构图

graph TD
  A[Redux Store] --> B[Provider]
  B -->|Context| C[React Components]
  C -->|useSelector| A
  C -->|useDispatch| A

Provider:状态注入的基石

核心作用与原理

import { Provider } from 'react-redux';
import store from './store';

function App() {
  return (
    <Provider store={store}>
      <MainApp />
    </Provider>
  );
}
  • 作用:通过 React Context 将 Redux store 注入整个组件树
  • 原理:使用 React.createContext() 创建 Store 上下文
  • 位置:必须在组件树最顶层

实现原理揭秘

// 简化版 Provider 实现
const StoreContext = React.createContext();

function Provider({ store, children }) {
  // 使用 useMemo 避免重复渲染
  const contextValue = useMemo(() => ({ store }), [store]);
  
  return (
    <StoreContext.Provider value={contextValue}>
      {children}
    </StoreContext.Provider>
  );
}

useSelector:精准状态选择

基础用法

import { useSelector } from 'react-redux';

function UserProfile() {
  // 从 Redux store 选择所需状态
  const user = useSelector(state => state.user);
  
  return <div>{user.name}</div>;
}

性能优化机制

// 默认使用 === 严格相等比较
const user = useSelector(state => state.user);

// 自定义比较函数
const user = useSelector(
  state => state.user,
  (prevUser, nextUser) => prevUser.id === nextUser.id
);

高级选择器模式

// 使用 reselect 库创建记忆化选择器
import { createSelector } from 'reselect';

const selectActiveUsers = createSelector(
  [state => state.users],
  users => users.filter(user => user.isActive)
);

function ActiveUsersList() {
  const activeUsers = useSelector(selectActiveUsers);
  // ...
}

useDispatch:触发状态更新

基础用法

import { useDispatch } from 'react-redux';

function AddTodoButton() {
  const dispatch = useDispatch();
  
  const handleClick = () => {
    dispatch({ type: 'ADD_TODO', text: 'New todo' });
  };
  
  return <button onClick={handleClick}>添加</button>;
}

封装动作创建函数

// 动作创建函数
const addTodo = (text) => ({ type: 'ADD_TODO', text });

// 组件中使用
function AddTodoButton() {
  const dispatch = useDispatch();
  
  const handleClick = () => {
    dispatch(addTodo('New todo')); // 直接 dispatch action
  };
  
  return <button onClick={handleClick}>添加</button>;
}

异步操作集成

// 使用 redux-thunk
const fetchUser = (id) => async dispatch => {
  dispatch({ type: 'USER_REQUEST' });
  const user = await api.getUser(id);
  dispatch({ type: 'USER_SUCCESS', payload: user });
};

function UserLoader() {
  const dispatch = useDispatch();
  
  useEffect(() => {
    dispatch(fetchUser(123));
  }, [dispatch]);
  
  // ...
}

三大 Hook 协作模式

完整数据流示例

function TodoList() {
  // 1. 获取状态
  const todos = useSelector(state => state.todos);
  
  // 2. 获取 dispatch 函数
  const dispatch = useDispatch();
  
  // 3. 事件处理
  const handleAdd = useCallback(() => {
    dispatch(addTodo('新任务'));
  }, [dispatch]);
  
  const handleToggle = useCallback(id => {
    dispatch(toggleTodo(id));
  }, [dispatch]);
  
  return (
    <div>
      <button onClick={handleAdd}>添加任务</button>
      <ul>
        {todos.map(todo => (
          <TodoItem 
            key={todo.id} 
            todo={todo} 
            onToggle={handleToggle}
          />
        ))}
      </ul>
    </div>
  );
}

connect 高阶组件(传统写法)

mapStateToProps

const mapStateToProps = (state, ownProps) => ({
  todos: state.todos,
  currentUser: state.users[ownProps.userId]
});

// 组件中通过 props 访问
function TodoList({ todos, currentUser }) {
  // ...
}

mapDispatchToProps

// 对象简写(自动绑定 dispatch)
const mapDispatchToProps = {
  addTodo,
  deleteTodo
};

// 函数形式
const mapDispatchToProps = (dispatch) => ({
  addTodo: (text) => dispatch(addTodo(text)),
  deleteTodo: (id) => dispatch(deleteTodo(id))
});

connect 使用

import { connect } from 'react-redux';

const ConnectedTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);

高频面试题与破解方案

Q:为什么需要 React-Redux?

  1. 解耦组件:避免组件直接依赖 Redux store
  2. 性能优化:自动实现 shouldComponentUpdate 优化
  3. 上下文管理:通过 Provider 优雅注入 store
  4. 简化开发:提供 useSelector/useDispatch 便捷 API
  5. 未来兼容:官方维护,保证与 React 新特性兼容

Q:useSelector 如何避免不必要重渲染?

优化策略:

// 策略1:返回原始值(非引用类型)
const count = useSelector(state => state.counter); // number

// 策略2:使用浅比较
const user = useSelector(state => state.user, shallowEqual);

// 策略3:记忆化选择器
const selectUser = useCallback(
  state => ({ name: state.user.name, id: state.user.id }), 
  []
);
const userInfo = useSelector(selectUser);

Q:如何在类组件中使用 Hooks?

解决方案:

// 创建连接组件
function withReduxHooks(WrappedComponent) {
  return function(props) {
    const data = useSelector(state => state.data);
    const dispatch = useDispatch();
    return <WrappedComponent {...props} data={data} dispatch={dispatch} />;
  };
}

// 使用
class LegacyComponent extends React.Component {
  // 通过 props 访问 data 和 dispatch
}

export default withReduxHooks(LegacyComponent);

总结:React-Redux 核心知识点

概念 作用 最佳实践
Provider 注入 Redux store 到组件树 包裹整个应用根组件
useSelector 从 store 选择状态 返回原始值 + 精细选择
useDispatch 获取 dispatch 方法 封装动作创建函数
connect 类组件连接方案(传统) 使用 mapState/mapDispatch
记忆化选择器 优化性能 使用 reselect 或 useMemo
Redux Toolkit 现代 Redux 开发标准 使用 createSlice + configureStore

5.5 现代状态管理方案(Recoil、Zustand、Jotai)

基础知识概述 (简要)

目标: 解决 React 应用中跨组件状态共享和复杂状态逻辑的管理问题,特别是当 Context + useState/useReducer 在性能或开发体验上显得不足时。

核心诉求:

  • 简化 API: 减少模板代码(boilerplate),提升开发效率。
  • 高性能更新: 精确控制组件更新范围,避免不必要的渲染。
  • 更好的 TypeScript 支持: 提供出色的类型推断。
  • 更自然的 React 集成: 拥抱 Hooks 和 React 的并发特性。
  • 更小的包体积: 减小应用体积。

与 Redux 的主要区别:

  • 通常不需要显式定义 action types, action creators, reducers。
  • 状态存储和更新逻辑更分散或原子化。
  • API 设计更贴近 React Hooks,学习曲线相对平缓。

Recoil (Meta 官方出品)

核心概念: 原子 (Atoms) 和 选择器 (Selectors)

  • Atom: 状态的最小单位。一个原子代表一份独立的状态(可以是任何类型)。组件通过 useRecoilState, useRecoilValue, useSetRecoilState 订阅或更新它。原子变化只触发订阅它的组件重渲染。
    import { atom } from 'recoil';
    const countState = atom({
      key: 'countState', // 必须全局唯一
      default: 0, // 初始值
    });
  • Selector: 派生状态。基于一个或多个 Atom 或其他 Selector 的值通过纯函数计算得出。组件同样使用 Hook 订阅。依赖的 Atom/Selector 变化时自动重新计算,并触发订阅组件更新。
    import { selector } from 'recoil';
    const doubledCountState = selector({
      key: 'doubledCountState',
      get: ({ get }) => {
        const count = get(countState);
        return count * 2;
      },
      // 可选:定义如何设置(会反向更新依赖的 Atom)
      set: ({ set }, newValue) => {
        set(countState, newValue / 2);
      }
    });

Hooks API:

  • useRecoilState(atom/selector): 类似于 useState,返回 [state, setter]。
  • useRecoilValue(atom/selector): 只读取状态值。
  • useSetRecoilState(atom/selector): 只获取设置状态的函数。
  • useRecoilCallback(): 用于在回调中异步读取/更新状态,避免闭包问题。

关键特性

  • 细粒度响应: 组件只订阅它真正需要的具体 Atom/Selector,状态变化时只有依赖这些状态的组件才会更新。这是解决 Redux 中 connect 或 useSelector 可能导致的“大范围更新”问题的关键。
  • Hooks-First API: 完全使用 React Hooks 的方式读取和修改状态,与函数组件无缝集成。
  • 异步支持: Selector 可以方便地处理异步数据获取和派生状态(如 useRecoilValueLoadable)。
  • 状态快照与时间旅行: 内置支持状态快照,便于调试(如 的 initializeState 和快照 API)。

优点

  • 概念相对清晰(Atom/Selector)。
  • 细粒度更新性能优秀。
  • 官方背景,社区和文档较好。
  • 内置异步支持良好。

缺点/考量

  • API 相对复杂: 理解 Atom、Selector 及其各种 Hook(useRecoilCallback, useRecoilValueLoadable, useRecoilTransaction_UNSTABLE 等)需要一定学习成本。
  • 仍在实验阶段 (Experimental): 虽然 Meta 内部广泛使用,但官方文档仍标记为 Experimental,API 稳定性有一定风险(尽管重大变更已较少)。
  • Bundle Size: 相对其他轻量方案稍大。

面试要点

  • 解释 Atom 和 Selector 的区别和联系。
  • 如何用 Selector 处理异步数据?
  • Recoil 如何实现细粒度更新?(依赖图追踪)
  • 对比 Recoil 与 Context + useState/useReducer 在性能上的差异(Context 更新会触发所有消费组件更新,Recoil 只更新依赖特定 Atom/Selector 的组件)。
  • 了解 atomFamily / selectorFamily 的作用(参数化状态)。

Zustand

核心思想:创建一个单一的、不可变的 Store。Store 包含状态和修改状态的方法(Action)。API 极其简洁。

核心 API(create)

import create from 'zustand';
// 类型提示 (TypeScript): import { create } from 'zustand'
const useStore = create((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  // 访问当前状态 (非更新时): get().count
  // 异步 Action 示例
  async fetchData: async () => {
    const response = await fetch('/api/data');
    set({ data: await response.json() }); // 更新状态
  }
}));

Hooks API

  • useStore(): 默认会订阅整个 Store 的变化(不推荐,可能导致过多渲染)。
  • useStore(selector): 推荐用法。通过一个选择器函数从 Store 中选取需要的部分状态或 Action。组件只在选择器返回的部分发生变化时才会重新渲染。Zustand 会对选择器结果进行严格的浅比较 (Object.is)。使用选择器函数是性能优化的关键。
    // 组件A: 只需要 count 和 increment
    function CounterA() {
      const { count, increment } = useStore(state => ({
        count: state.count,
        increment: state.increment
      }));
      // ... render
    }
    // 组件B: 只需要 decrement
    function CounterB() {
      const decrement = useStore(state => state.decrement);
      // ... render
    }

关键特性/优势

  • 极致简洁: API 极其精简,学习成本低,样板代码极少。
  • 高性能: 精确的订阅机制(通过选择器)确保组件只在相关状态变化时更新。
  • 无 Provider 地狱: Store 是全局单例(虽然可以创建多个 Store),不需要用 Context Provider 包裹应用根节点。组件可以直接在任何地方使用 useStore。
  • Middleware 支持: 内置支持中间件(如 persist 持久化、immer 简化不可变更新、devtools 连接 Redux DevTools)。
  • 出色的 TS 支持: 类型推断非常友好。

适用场景

  • 绝大多数应用场景,特别是追求简洁、高效、易用的项目。从中小型到大型项目都适用。是目前非常流行的 Redux 替代品。

Jotai

核心思想: 受 Recoil 的 Atom 概念启发,但 API 设计更精简和原始(Primitive)。完全拥抱 React 的 Context 和 Hooks 思想。

核心概念

  • Atom: 状态的基本单位。定义一个包含初始值的 Atom(不需要唯一的全局 key!)。
    import { atom } from 'jotai';
    const countAtom = atom(0); // 初始值
  • Derived Atom: 通过读取其他 Atom 的值计算而来。可以是只读的或可写的(通过定义一个 get 函数和一个可选的 set 函数)。
    const doubledCountAtom = atom((get) => get(countAtom) * 2); // 只读派生
    // 可写派生 (set 会更新依赖的原始 atom)
    const countPlusOneAtom = atom(
      (get) => get(countAtom) + 1, // getter
      (get, set, newValue) => { set(countAtom, newValue - 1) } // setter
    );

Hooks API

  • useAtom(atom): 核心 Hook。返回 [value, setter]。组件会订阅传递给 useAtom 的这个特定 Atom 的变化。
    function Counter() {
      const [count, setCount] = useAtom(countAtom);
      // ... render
    }

Provider 与 Context

  • 默认情况下,Atom 状态是全局共享的(通过 Jotai 内部的默认 Context)。
  • Provider: 如果需要创建隔离的状态作用域(例如在微前端、测试或需要多个独立实例的场景),可以使用 <Provider> 包裹组件树。Provider 内的组件访问的是该 Provider 作用域内的 Atom 状态。
    import { Provider } from 'jotai';
    function App() {
      return (
        <Provider>
          <ComponentThatUsesAtoms />
        </Provider>
      );
    }

关键特性/优势

  • 极简 API: 核心 API 只有 atom 和 useAtom。概念极其精简。
  • 灵活的组合性: Atom 可以自由组合,构建复杂状态逻辑。
  • 无 Key 设计: 不需要为 Atom 定义全局唯一的 key,简化了定义和使用。
  • 基于 Context: 底层利用 React Context 实现,但通过优化避免了 Context 的渲染爆炸问题。Provider 提供了状态隔离能力。
  • 轻量级: 包体积非常小。
  • 优秀的并发模式兼容性。

适用场景

  • 喜欢极简 API、原子化思想,且可能需要状态隔离(Provider)的项目。中小型到大型项目均可。

对比与选型建议 (面试高频)

特性 Recoil Zustand Jotai
核心模型 Atoms + Selectors (Graph) Centralized Store + Actions Atoms (Primitive)
API 复杂度 中等 (概念较多) 极低 (非常简洁) 很低 (极其精简)
学习曲线 中等偏上
状态更新粒度 细粒度 (Atom/Selector 级) 细粒度 (Selector 函数级) 细粒度 (Atom 级)
Provider 要求 <RecoilRoot> 必需 不需要 (全局 Store) 可选 (默认全局,可隔离)
异步处理 强大 (Selector + Suspense) 良好 (在 Action 中处理) 良好 (在 Atom 或 Effect 中处理)
TypeScript 优秀 极佳 极佳
包体积 较大 很小 极小
开发体验 (DX) 好 (Meta 工具) 极佳 (简洁,DevTools) 极佳 (简洁)
适用规模 大型 / 复杂派生状态 全规模 (尤其推荐) 全规模 (推荐中小到中大型)
类似库 - Valtio, React Tracked Recoil (简化版)

面试踩坑点与高频问题

  • useRecoilValueLoadable 状态 (state, contents)? Recoil 异步 Selector 使用 useRecoilValueLoadable 获取 Loadable 对象,其 state 表示状态 (‘hasValue’, ‘loading’, ‘hasError’),contents 包含实际值或 Error 对象。
  • Zustand 性能关键 (selector 函数): 务必使用带选择器函数的 useStore(selector) 来避免订阅整个 Store 导致的无效渲染。强调选择器结果变化才会触发渲染。
  • Jotai 的 Provider 作用: 理解默认全局 Context 和 创建隔离作用域的区别及适用场景(微前端、测试、组件库)。
  • 不可变更新 (所有库): 虽然 Zustand 和 Jotai 在 set 时可以“直接修改”,但最佳实践仍是保持不可变性(尤其数组/对象),以避免潜在的渲染问题或 DevTools 跟踪问题。Zustand 常配合 immer 中间件。
  • 闭包陷阱 (异步 Action): 在 Zustand/Jotai 的异步 Action 中,如果依赖当前状态进行计算,应使用 get() 函数 (Zustand) 或 get 参数 (Jotai 的 Atom set),而不是直接读取闭包中的旧状态值。
    • 错误示例 (Zustand 闭包陷阱):
      // ❌ 错误:闭包可能捕获旧的 count
      const useStore = create((set) => ({
        count: 0,
        incrementAsync: async () => {
          await delay(1000);
          set({ count: count + 1 }); // 这里的 `count` 是创建时的初始值 0!
        }
      }));
    • 正确做法 (使用 get):
      // ✅ 正确:通过 get 访问最新状态
      const useStore = create((set, get) => ({
        count: 0,
        incrementAsync: async () => {
          await delay(1000);
          set({ count: get().count + 1 }); // 获取当前最新值
        }
      }));
  • 与 React 18 并发特性的兼容性: 强调 Recoil 和 Jotai 在设计上考虑了并发渲染。Zustand 也基本兼容,但需注意在严格模式下的双重渲染可能带来的副作用问题(可通过中间件或状态稳定化处理)。
  • 为什么不用 Context 代替? Context 在状态更新时会导致所有消费该 Context 的组件重新渲染,无论它们是否依赖于变化的那部分状态。这在频繁更新或大型状态对象时性能极差。Recoil/Zustand/Jotai 都通过订阅机制实现了细粒度更新。

第六章:React Router 与数据请求

6.1 React Router 基础(BrowserRouter、Route、Link)

基础概念(简要)

SPA 路由原理

  • 在单页应用中,路由切换不会触发页面刷新
  • 通过监听 URL 变化(hash 或 history API),动态渲染对应组件
  • 保持应用状态的同时实现视图切换

React Router 核心作用

  • 声明式路由配置
  • URL 与组件的映射关系管理
  • 导航控制(声明式 <Link> 和编程式 navigate)

核心组件深度解析(React Router v6)

<BrowserRouter>

// 入口文件 index.js
import { BrowserRouter } from 'react-router-dom';

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
  • 作用:使用 HTML5 History API (pushState, replaceState) 管理路由
  • 特点:
    • URL 格式为干净的路径(如 /dashboard)
    • 需要服务器配合:所有路由请求需返回 index.html
    • 面试坑点:直接访问子路由出现 404 的解决方案(配置服务器 fallback)

<Routes><Route>

import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      {/* 基础路由 */}
      <Route path="/" element={<HomePage />} />
      
      {/* 动态路由 */}
      <Route path="/users/:id" element={<UserDetail />} />
      
      {/* 嵌套路由 */}
      <Route path="/dashboard" element={<DashboardLayout />}>
        <Route index element={<DashboardHome />} /> {/* index 路由 */}
        <Route path="settings" element={<SettingsPage />} />
      </Route>
      
      {/* 404 路由 */}
      <Route path="*" element={<NotFoundPage />} />
    </Routes>
  );
}
  • <Routes>
    • 路由匹配容器,替代 v5 的 <Switch>
    • 自动选择最佳匹配的路由(不再按顺序匹配)
  • <Route> 关键属性:
    • path:URL 匹配规则(支持 :id 动态参数)
    • element:匹配时渲染的组件(必须是 React 元素,如 <Component />
    • index:标识默认子路由(当父路由匹配时渲染)

<Link><NavLink>

import { Link, NavLink } from 'react-router-dom';

// 基础链接
<Link to="/about">关于我们</Link>

// 带状态的导航
<Link 
  to="/users/123"
  state={{ fromDashboard: true }}  // 传递状态
>
  用户详情
</Link>

// 活动链接样式
<NavLink 
  to="/dashboard"
  style={({ isActive }) => ({ 
    color: isActive ? 'red' : 'blue' 
  })}
  end  // 精确匹配(v6 新特性)
>
  控制台
</NavLink>
  • <Link>
    • 声明式导航组件,渲染为 <a> 标签
    • to 属性支持字符串/对象(可包含 pathname, search, hash, state)
  • <NavLink>
    • 增强版 <Link>,可添加活动状态样式
    • isActive 标志当前是否匹配
    • end 属性确保精确匹配(避免父路由激活时子路由也被激活)

基础路由配置模式

集中式配置(推荐)

// routes.js
const routes = [
  { path: '/', element: <Home /> },
  { 
    path: '/products',
    element: <ProductLayout />,
    children: [
      { index: true, element: <ProductList /> },
      { path: ':id', element: <ProductDetail /> }
    ]
  }
];

// App.js
import { useRoutes } from 'react-router-dom';

function App() {
  return useRoutes(routes);
}

编程式导航

import { useNavigate } from 'react-router-dom';

function LoginButton() {
  const navigate = useNavigate();
  
  const handleLogin = () => {
    login().then(() => {
      // 跳转并替换历史记录(不可回退)
      navigate('/dashboard', { replace: true, state: { user } });
    });
  };

  return <button onClick={handleLogin}>登录</button>;
}
  • useNavigate() 返回导航函数
  • 参数选项:
    • replace: true 替换当前历史记录
    • state 传递路由状态(可通过 useLocation() 获取)

面试高频问题与踩坑点

BrowserRouter 的服务器配置问题

问题:直接访问 /dashboard 路由出现 404

解决方案:

# Nginx 配置
location / {
  try_files $uri $uri/ /index.html;
}

v5 到 v6 的重大变化

  • 废除 <Switch> → 改用 <Routes>
  • 废除 component={Component} → 改用 element={<Component />}
  • 废除 exact 属性 → 路径匹配规则变更(默认部分匹配)
  • 嵌套路由必须使用 <Outlet> 作为占位符

动态路由参数获取

import { useParams } from 'react-router-dom';

function UserDetail() {
  const { id } = useParams();  // 获取 :id 参数
  // ...
}

路由状态传递与获取

// 发送方
<Link to="/profile" state={{ fromHome: true }} />

// 接收方
import { useLocation } from 'react-router-dom';

function Profile() {
  const location = useLocation();
  console.log(location.state?.fromHome); // true
}

<NavLink> 精确匹配问题

// 错误:/dashboard/settings 会同时激活两个链接
<NavLink to="/dashboard">控制台</NavLink>
<NavLink to="/dashboard/settings">设置</NavLink>

// 正确:父链接添加 end 属性
<NavLink to="/dashboard" end>控制台</NavLink>

路由鉴权基础模式

// 高阶组件示例
const PrivateRoute = ({ children }) => {
  const { user } = useAuth();
  return user ? children : <Navigate to="/login" />;
};

// 使用
<Route 
  path="/admin"
  element={
    <PrivateRoute>
      <AdminPanel />
    </PrivateRoute>
  }
/>

核心总结

  • 路由三件套:
    • <BrowserRouter> 提供路由上下文
    • <Routes> + <Route> 定义路由规则
    • <Link>/<NavLink> 实现声明式导航
  • v6 核心变化:
    • 路由配置从 component 改为 element
    • 嵌套路由使用 <Outlet> 作为渲染出口
    • 匹配规则优化(自动选择最佳匹配)
  • 面试必考:
    • 动态路由参数获取(useParams)
    • 编程式导航(useNavigate)
    • 路由守卫实现方案
    • v5 到 v6 的迁移问题

6.2 动态路由与嵌套路由

核心概念

动态路由

  • 定义:路径中包含参数的路由(如 /users/:id)
  • 核心价值:
    • 创建可复用的视图模板
    • 根据URL参数动态加载数据
    • 实现SEO友好的URL结构

嵌套路由

  • 定义:路由之间的父子层级关系
  • 核心价值:
    • 实现布局复用(共享header/sidebar)
    • 组织复杂的应用结构
    • 保持局部UI状态(仅更新子路由区域)

动态路由深度解析

基础实现

// 路由配置
<Routes>
  <Route path="/products/:productId" element={<ProductDetail />} />
</Routes>

// 组件内获取参数
import { useParams } from 'react-router-dom';

function ProductDetail() {
  const { productId } = useParams(); // 获取动态参数
  // 使用 productId 加载数据...
}

高级匹配模式

// 多参数路由
<Route path="/category/:categoryId/product/:productId" />

// 可选参数
<Route path="/search/:keyword?/:filter?" />

// 正则约束 (v6.8+)
<Route 
  path="/user/:id(\\d+)" // 只匹配数字ID
  element={<UserProfile />} 
/>

数据加载模式

// React Router v6.4+ 数据路由
const router = createBrowserRouter([
  {
    path: "products/:id",
    element: <ProductLayout />,
    loader: async ({ params }) => {
      return fetch(`/api/products/${params.id}`); // 预加载数据
    },
    children: [
      { index: true, element: <ProductOverview /> },
      { path: "reviews", element: <ProductReviews /> }
    ]
  }
]);

嵌套路由深度解析

基础嵌套结构

<Routes>
  <Route path="dashboard" element={<DashboardLayout />}>
    <Route index element={<DashboardHome />} />
    <Route path="analytics" element={<AnalyticsPage />} />
    <Route path="settings" element={<SettingsPage />} />
  </Route>
</Routes>

// DashboardLayout.jsx
import { Outlet } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div className="dashboard">
      <Sidebar />
      <div className="content">
        <Outlet /> {/* 子路由渲染位置 */}
      </div>
    </div>
  );
}

多级嵌套实现

<Route path="admin" element={<AdminLayout />}>
  <Route index element={<AdminDashboard />} />
  <Route path="users" element={<UserManagementLayout />}>
    <Route index element={<UserList />} />
    <Route path=":userId" element={<UserDetail />} />
  </Route>
</Route>

布局路由模式

// 创建共享布局组件
function MainLayout() {
  return (
    <>
      <Header />
      <main>
        <Outlet />
      </main>
      <Footer />
    </>
  );
}

// 应用布局
<Route element={<MainLayout />}>
  <Route path="/" element={<HomePage />} />
  <Route path="/about" element={<AboutPage />} />
  <Route path="/contact" element={<ContactPage />} />
</Route>

关键特性与最佳实践

动态路径生成

// 根据数据生成动态路由
const products = await fetchProducts();

const routes = products.map(product => ({
  path: `products/${product.slug}`,
  element: <ProductPage />,
  loader: () => product
}));

相对路由导航

// 在 /dashboard/settings 中
function SettingsPage() {
  return (
    <div>
      <Link to="../analytics">返回分析</Link> 
      {/* 指向 /dashboard/analytics */}
    </div>
  );
}

错误边界处理

// 路由级错误处理
<Route
  path="projects/:projectId"
  element={<ProjectDetail />}
  errorElement={<ProjectError />} // 捕获组件渲染错误
  loader={async ({ params }) => {
    const project = await fetchProject(params.projectId);
    if (!project) throw new Response("Not Found", { status: 404 });
    return project;
  }}
/>

面试高频问题与解决方案

动态路由数据加载竞态条件

问题:快速切换路由时,后发请求可能先返回导致数据错乱

解决方案:

useEffect(() => {
  let isActive = true;
  
  const fetchData = async () => {
    const data = await fetch(`/api/products/${productId}`);
    if (isActive) setProduct(data);
  };

  fetchData();
  
  return () => { isActive = false; }; // 清理函数取消旧请求
}, [productId]);

嵌套路由权限控制

// 路由守卫组件
const ProtectedRoute = ({ children, roles }) => {
  const { user } = useAuth();
  
  if (!user) return <Navigate to="/login" />;
  if (roles && !roles.includes(user.role)) 
    return <Navigate to="/unauthorized" />;
  
  return children;
};

// 使用
<Route 
  path="admin"
  element={
    <ProtectedRoute roles={['admin', 'superadmin']}>
      <AdminLayout />
    </ProtectedRoute>
  }
>
  {/* 子路由自动继承保护 */}
</Route>

面包屑导航实现

// 使用 useMatches() 获取路由层级
function Breadcrumbs() {
  const matches = useMatches();
  
  return (
    <nav>
      {matches
        .filter(match => match.handle?.breadcrumb)
        .map(match => (
          <span key={match.pathname}>
            {match.handle.breadcrumb(match)}
          </span>
        ))}
    </nav>
  );
}

// 路由配置中添加元数据
<Route 
  path="products/:id"
  element={<ProductPage />}
  handle={{ breadcrumb: (match) => `Product ${match.params.id}` }}
/>

滚动位置恢复

// 创建自定义滚动管理
function ScrollToTop() {
  const location = useLocation();
  
  useLayoutEffect(() => {
    // 排除需要保留滚动位置的路由
    if (!location.state?.preserveScroll) {
      window.scrollTo(0, 0);
    }
  }, [location.key]);
  
  return null;
}

// 在根路由使用
<Router>
  <ScrollToTop />
  <Routes>...</Routes>
</Router>

性能优化技巧

路由级代码分割

const ProductDetail = lazy(() => import('./ProductDetail'));

<Route 
  path="products/:id" 
  element={
    <Suspense fallback={<Loading />}>
      <ProductDetail />
    </Suspense>
  }
/>

预加载策略

// 鼠标悬停时预加载路由组件
<Link 
  to="/dashboard/analytics"
  onMouseEnter={() => import('./AnalyticsPage')}
>
  分析面板
</Link>

总结

动态路由核心

  • 使用 :param 语法定义动态段
  • 通过 useParams() 获取参数
  • 支持正则约束和可选参数

嵌套路由核心

  • 使用 <Outlet> 作为子路由渲染出口
  • 布局路由共享UI结构
  • 路由配置反映UI层次关系

高级模式

  • 数据路由(v6.4+)实现预加载
  • 相对导航简化路径管理
  • 错误边界增强健壮性

面试重点

  • 动态路由的数据加载问题
  • 嵌套路由的权限控制方案
  • 复杂路由结构设计能力
  • 性能优化实践

6.3 路由守卫与权限控制

权限控制核心概念

权限控制类型

类型 场景 实现难度
路由级守卫 整个路由模块的访问控制 ★★☆
组件级守卫 页面内特定功能的访问控制 ★★★
数据级守卫 API 请求和数据显示控制 ★★★★

认证与授权

  • 认证(Authentication):验证用户身份(是否登录)
  • 授权(Authorization):验证用户权限(是否有权访问)

基础路由守卫实现

高阶组件模式 (HOC)

// 路由守卫高阶组件
const withAuth = (Component, requiredRole = null) => {
  return (props) => {
    const { user, isAuthenticated } = useAuth();
    const location = useLocation();
    
    if (!isAuthenticated) {
      // 未登录重定向到登录页
      return <Navigate to="/login" state={{ from: location }} replace />;
    }
    
    if (requiredRole && user.role !== requiredRole) {
      // 权限不足
      return <Navigate to="/unauthorized" replace />;
    }
    
    return <Component {...props} />;
  };
};

// 使用示例
const AdminDashboard = withAuth(DashboardComponent, 'admin');

// 路由配置
<Route 
  path="/admin" 
  element={<AdminDashboard />} 
/>

包装器组件模式

// 路由守卫组件
const RouteGuard = ({ children, roles }) => {
  const { user, isAuthenticated } = useAuth();
  const location = useLocation();
  
  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  if (roles && !roles.includes(user.role)) {
    return <div className="unauthorized">
      <h2>403 Forbidden</h2>
      <p>您没有权限访问此页面</p>
    </div>;
  }
  
  return children;
};

// 使用示例
<Route 
  path="/dashboard" 
  element={
    <RouteGuard roles={['user', 'admin']}>
      <DashboardPage />
    </RouteGuard>
  } 
/>

高级权限控制方案

基于路由配置的集中式管理

// 路由配置文件
const routes = [
  {
    path: '/',
    element: <HomePage />,
    public: true
  },
  {
    path: '/dashboard',
    element: <DashboardLayout />,
    roles: ['user', 'admin'],
    children: [
      { path: 'analytics', element: <Analytics /> },
      { 
        path: 'admin', 
        element: <AdminPanel />,
        roles: ['admin'] // 子路由可以覆盖父路由权限
      }
    ]
  },
  {
    path: '/system-settings',
    element: <SystemSettings />,
    roles: ['superadmin']
  }
];

// 路由渲染器
const renderRoutes = (routes) => {
  return routes.map((route) => (
    <Route
      key={route.path}
      path={route.path}
      element={
        route.public ? (
          route.element
        ) : (
          <RouteGuard roles={route.roles}>
            {route.element}
          </RouteGuard>
        )
      }
    >
      {route.children && renderRoutes(route.children)}
    </Route>
  ));
};

RBAC(基于角色的访问控制)

// 权限服务
const permissionService = {
  // 角色权限映射
  roles: {
    guest: ['view_public'],
    user: ['view_dashboard', 'edit_profile'],
    admin: ['manage_users', 'view_reports', 'edit_settings'],
    superadmin: ['*'] // 所有权限
  },
  
  // 检查权限
  hasPermission(user, requiredPermission) {
    if (!user || !user.role) return false;
    
    const userPermissions = this.roles[user.role] || [];
    
    return userPermissions.includes('*') || 
           userPermissions.includes(requiredPermission);
  }
};

// 权限检查钩子
const usePermission = (requiredPermission) => {
  const { user } = useAuth();
  
  return permissionService.hasPermission(user, requiredPermission);
};

// 使用示例
function AnalyticsPage() {
  const canViewReports = usePermission('view_reports');
  
  return (
    <div>
      {canViewReports ? (
        <AnalyticsChart />
      ) : (
        <div className="permission-warning">
          您没有查看报表的权限
        </div>
      )}
    </div>
  );
}

ABAC(基于属性的访问控制)

// ABAC 策略引擎
const abacEngine = {
  evaluate(policy, context) {
    // 简化版策略评估
    return policy.rules.every(rule => {
      const value = context[rule.attribute];
      switch (rule.operator) {
        case 'eq': return value == rule.value;
        case 'gt': return value > rule.value;
        case 'in': return rule.value.includes(value);
        default: return false;
      }
    });
  }
};

// 项目访问权限检查
const useProjectAccess = (projectId) => {
  const { user } = useAuth();
  const [project] = useFetchProject(projectId);
  
  const canEdit = useMemo(() => {
    if (!user || !project) return false;
    
    const context = {
      userId: user.id,
      userRole: user.role,
      projectOwner: project.ownerId,
      projectStatus: project.status,
      teamMembers: project.teamMembers
    };
    
    const editPolicy = {
      rules: [
        { attribute: 'userRole', operator: 'in', value: ['admin', 'pm'] },
        { 
          attribute: 'projectStatus', 
          operator: 'neq', 
          value: 'archived' 
        },
        {
          anyOf: [
            { attribute: 'projectOwner', operator: 'eq', value: user.id },
            { attribute: 'teamMembers', operator: 'in', value: user.id }
          ]
        }
      ]
    };
    
    return abacEngine.evaluate(editPolicy, context);
  }, [user, project]);
  
  return { canEdit };
};

企业级最佳实践

路由元数据方案 (React Router 6.4+)

// 使用loader实现权限控制
const router = createBrowserRouter([
  {
    path: "/admin",
    element: <AdminLayout />,
    loader: async () => {
      // 权限检查
      const { user } = await authService.getCurrentUser();
      
      if (!user) {
        throw redirect("/login");
      }
      
      if (user.role !== "admin") {
        throw new Response("Forbidden", { status: 403 });
      }
      
      return { user };
    },
    errorElement: <AdminErrorBoundary />,
    children: [
      { index: true, element: <AdminDashboard /> },
      { path: "users", element: <UserManagement /> }
    ]
  }
]);

JWT 认证集成方案

// Axios 请求拦截器
api.interceptors.request.use(config => {
  const token = localStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Axios 响应拦截器 (处理token刷新)
api.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // 刷新token
        const { data } = await refreshToken();
        localStorage.setItem('access_token', data.access_token);
        
        // 重试原始请求
        originalRequest.headers.Authorization = `Bearer ${data.access_token}`;
        return api(originalRequest);
      } catch (refreshError) {
        // 刷新失败,跳转登录
        store.dispatch(logout());
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

权限指令式组件

// 权限控制组件
const Permission = ({ 
  children, 
  requiredPermission,
  fallback = null 
}) => {
  const hasPermission = usePermission(requiredPermission);
  
  if (!hasPermission) {
    return fallback;
  }
  
  return children;
};

// 使用示例
function UserManagementPage() {
  return (
    <div>
      <h1>用户管理</h1>
      
      <Permission requiredPermission="create_user">
        <button>创建新用户</button>
      </Permission>
      
      <Permission 
        requiredPermission="delete_user"
        fallback={<p class="text-muted">您无权删除用户</p>}
      >
        <UserTable withActions />
      </Permission>
    </div>
  );
}

面试高频问题解决方案

Q:路由切换时的权限验证

问题:用户权限变更后,已打开页面的权限未更新

解决方案:事件总线通知 + 全局状态监听

// 权限变更事件
const AuthEvents = {
  PERMISSION_CHANGED: 'permission_changed'
};

// 在权限服务中
authService.onPermissionsChanged = () => {
  eventBus.emit(AuthEvents.PERMISSION_CHANGED);
};

// 在路由守卫组件中
const RouteGuard = ({ children, roles }) => {
  const [permissionsValid, setPermissionsValid] = useState(true);
  
  useEffect(() => {
    const handler = () => {
      setPermissionsValid(checkPermissions(roles));
    };
    
    eventBus.on(AuthEvents.PERMISSION_CHANGED, handler);
    return () => eventBus.off(handler);
  }, [roles]);
  
  if (!permissionsValid) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  return children;
};

Q:按钮级权限的防抖控制

问题:频繁点击权限按钮导致多次权限检查请求

解决方案:权限缓存 + 防抖机制

const useCachedPermission = (permission, ttl = 5000) => {
  const [cache, setCache] = useState({});
  
  const check = useCallback(async () => {
    const now = Date.now();
    
    if (cache[permission] && cache[permission].expires > now) {
      return cache[permission].result;
    }
    
    const result = await api.checkPermission(permission);
    setCache(prev => ({
      ...prev,
      [permission]: { result, expires: now + ttl }
    }));
    
    return result;
  }, [permission, ttl, cache]);
  
  return check;
};

// 使用示例
function DeleteButton() {
  const checkDeletePermission = useCachedPermission('delete_item');
  const [disabled, setDisabled] = useState(true);
  
  useEffect(() => {
    checkDeletePermission().then(hasPermission => {
      setDisabled(!hasPermission);
    });
  }, []);
  
  return (
    <button 
      disabled={disabled}
      onClick={handleDelete}
    >
      删除
    </button>
  );
}

Q:动态菜单权限控制

// 动态生成菜单
const useAuthorizedMenu = () => {
  const { user } = useAuth();
  const [menuItems, setMenuItems] = useState([]);
  
  useEffect(() => {
    const loadMenu = async () => {
      const allItems = await api.getMenuItems();
      
      const authorizedItems = await Promise.all(
        allItems.map(async item => {
          if (!item.permission) return item;
          
          const hasPerm = await permissionService.checkPermission(
            item.permission
          );
          return hasPerm ? item : null;
        })
      );
      
      setMenuItems(authorizedItems.filter(Boolean));
    };
    
    loadMenu();
  }, [user]);
  
  return menuItems;
};

// 菜单组件
function AppSidebar() {
  const menuItems = useAuthorizedMenu();
  
  return (
    <nav>
      <ul>
        {menuItems.map(item => (
          <li key={item.path}>
            <Link to={item.path}>{item.title}</Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

安全增强措施

敏感数据保护

// 使用权限hook封装数据请求
const useProtectedData = (endpoint, requiredPermission) => {
  const hasPermission = usePermission(requiredPermission);
  const [data, setData] = useState(null);
  
  useEffect(() => {
    if (hasPermission) {
      fetch(endpoint)
        .then(res => res.json())
        .then(setData);
    } else {
      setData(null); // 清除已有数据
    }
  }, [hasPermission, endpoint]);
  
  return hasPermission ? data : null;
};

路由权限审计日志

// 在路由守卫中添加日志
const RouteGuard = ({ children, roles }) => {
  // ...权限检查逻辑
  
  useEffect(() => {
    if (isAuthenticated) {
      auditService.logRouteAccess(
        location.pathname, 
        user.id,
        hasPermission ? 'GRANTED' : 'DENIED'
      );
    }
  }, [location]);
  
  // ...
};

权限变更强制刷新

// 监听权限变更
useEffect(() => {
  const handlePermissionChange = () => {
    // 显示刷新提示
    setShowRefresh(true);
  };
  
  permissionService.onChange(handlePermissionChange);
  return () => permissionService.offChange(handlePermissionChange);
}, []);

// 在UI中显示提示
{showRefresh && (
  <div className="refresh-banner">
    您的权限已更新,请
    <button onClick={() => window.location.reload()}>
      刷新页面
    </button>
    以应用更改
  </div>
)}

总结

  • 核心模式:
    • 高阶组件(HOC)模式:适合类组件场景
    • 包装器组件:函数组件推荐方案
    • 路由元数据:React Router 6.4+最佳实践
  • 权限模型:
    • RBAC:基于角色的权限控制(适合大多数场景)
    • ABAC:基于属性的动态权限控制(复杂系统适用)
  • 企业级方案:
    • JWT认证集成
    • 权限变更实时响应
    • 动态菜单控制
    • 审计日志追踪
  • 面试重点:
    • 路由级 vs 组件级权限控制
    • 权限变更后的状态同步
    • 按钮级权限的性能优化
    • 动态菜单的实现方案

6.4 数据请求方案(fetch、axios、React Query、SWR)

数据请求核心方案对比

方案 特点 适用场景 包大小
原生 fetch 浏览器原生 API,无需安装 简单请求、现代浏览器项目 0kB
Axios 功能丰富、拦截器、自动转换 JSON 企业级应用、复杂请求处理 4.8kB
React Query 数据缓存、自动重试、请求管理 复杂数据交互、实时应用 11.8kB
SWR 轻量级、快速集成、智能缓存 快速开发、中小型项目 3.5kB

基础请求方案深度解析

Fetch API

// 基础 GET 请求
const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch error:', error);
    throw error;
  }
};

// POST 请求示例
const postData = async (payload) => {
  const response = await fetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${token}`
    },
    body: JSON.stringify(payload)
  });
  return response.json();
};

核心特性:

  • 优点:浏览器原生支持、Promise 接口
  • 缺点:无请求取消、无超时控制(需手动实现)
  • 关键配置:
    // 超时控制
    const fetchWithTimeout = (url, options = {}, timeout = 8000) => {
      return Promise.race([
        fetch(url, options),
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error('Request timeout')), timeout)
        )
      ]);
    };

Axios 高级应用

import axios from 'axios';

// 创建实例
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {'X-Custom-Header': 'value'}
});

// 请求拦截器
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
}, error => Promise.reject(error));

// 响应拦截器
api.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      // 处理未授权
      window.location = '/login';
    }
    return Promise.reject(error);
  }
);

// 取消令牌
const source = axios.CancelToken.source();
api.get('/data', { cancelToken: source.token });
source.cancel('Operation canceled by user');

核心特性:

  • 自动转换 JSON 数据
  • 客户端 XSRF 防护
  • 并发请求支持 (axios.all)
  • 上传进度监控
    api.post('/upload', formData, {
      onUploadProgress: progressEvent => {
        const percent = Math.round(
          (progressEvent.loaded * 100) / progressEvent.total
        );
        console.log(`Upload progress: ${percent}%`);
      }
    });

现代数据管理库深度解析

React Query 核心概念

import { useQuery, useMutation, QueryClient, QueryClientProvider } from 'react-query';

// 创建查询客户端
const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserProfile />
    </QueryClientProvider>
  );
}

// 查询示例
function UserProfile() {
  const { data, isLoading, isError, error } = useQuery(
    'userData', 
    () => api.get('/users/123'),
    {
      staleTime: 5 * 60 * 1000, // 5分钟保鲜期
      retry: 2, // 失败重试次数
      refetchOnWindowFocus: true // 窗口聚焦时重新获取
    }
  );

  // 突变示例
  const updateUser = useMutation(
    (newData) => api.put('/users/123', newData),
    {
      onSuccess: () => {
        // 更新成功后重新获取用户数据
        queryClient.invalidateQueries('userData');
      },
      onError: (error) => {
        toast.error(`更新失败: ${error.message}`);
      }
    }
  );

  if (isLoading) return <Spinner />;
  if (isError) return <Error message={error.message} />;
  
  return (
    <div>
      <h1>{data.name}</h1>
      <button onClick={() => updateUser.mutate({ name: '新名字' })}>
        更新用户
      </button>
    </div>
  );
}

核心特性:

  • 智能缓存:自动管理请求缓存
  • 后台刷新:数据过期时在后台更新
  • 分页查询:
    const { data, fetchNextPage } = useInfiniteQuery(
      'projects',
      ({ pageParam = 1 }) => api.get(`/projects?page=${pageParam}`),
      {
        getNextPageParam: (lastPage) => lastPage.nextPage
      }
    );

SWR 核心应用

import useSWR from 'swr';

// 自定义 fetcher
const fetcher = (...args) => fetch(...args).then(res => res.json());

function Profile() {
  const { data, error, mutate } = useSWR(
    '/api/user/123', 
    fetcher,
    {
      revalidateOnFocus: false,
      refreshInterval: 60000, // 60秒刷新一次
      onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
        // 404错误不重试
        if (error.status === 404) return;
        // 10秒后重试
        setTimeout(() => revalidate({ retryCount }), 10000);
      }
    }
  );

  // 乐观更新
  const updateName = async (newName) => {
    // 立即更新本地数据
    mutate({ ...data, name: newName }, false);
    
    try {
      await api.patch('/api/user/123', { name: newName });
      // 重新验证确保数据最新
      mutate();
    } catch (err) {
      // 出错时回滚
      mutate(data, false);
    }
  };

  return (
    <div>
      {data ? <h1>{data.name}</h1> : 'Loading...'}
      <button onClick={() => updateName('New Name')}>
        更新名字
      </button>
    </div>
  );
}

核心特性:

  • 请求去重:自动避免重复请求
  • 依赖请求:基于数据依赖的请求链
    const { data: user } = useSWR('/api/user/123', fetcher);
    const { data: projects } = useSWR(
      () => `/api/projects?userId=${user.id}`, 
      fetcher
    );
  • SSR 支持:
    export async function getServerSideProps() {
      const user = await fetcher('/api/user/123');
      return { props: { fallback: { '/api/user/123': user } } };
    }
    
    function Page({ fallback }) {
      return (
        <SWRConfig value={{ fallback }}>
          <Profile />
        </SWRConfig>
      );
    }

高级数据管理策略

请求状态统一管理

// 封装 useApi 钩子
const useApi = (key, fn, options = {}) => {
  const [status, setStatus] = useState('idle');
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  
  const execute = useCallback(async (params) => {
    setStatus('pending');
    try {
      const result = await fn(params);
      setData(result);
      setStatus('success');
      return result;
    } catch (err) {
      setError(err);
      setStatus('error');
      throw err;
    }
  }, [fn]);

  return {
    status,  // 'idle' | 'pending' | 'success' | 'error'
    data,
    error,
    execute,
    isLoading: status === 'pending',
    isError: status === 'error'
  };
};

// 使用示例
const { data, status, execute: fetchUser } = useApi(
  'userData',
  (userId) => api.get(`/users/${userId}`)
);

数据缓存策略

// 使用 React Query 的缓存工具
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 15 * 60 * 1000, // 15分钟缓存
      staleTime: 5 * 60 * 1000,  // 5分钟保鲜
    }
  }
});

// 手动管理缓存
const updateCache = (newUser) => {
  queryClient.setQueryData(['user', newUser.id], newUser);
  
  // 更新列表缓存
  queryClient.setQueryData(['users'], (old) => 
    old.map(user => user.id === newUser.id ? newUser : user)
  );
};

// SWR 全局配置
<SWRConfig 
  value={{
    provider: () => new Map(),
    isOnline() {
      // 自定义网络状态检测
      return navigator.onLine;
    }
  }}
>
  <App />
</SWRConfig>

面试高频问题解决方案

请求竞态问题(Race Condition)

问题:快速切换参数导致旧请求覆盖新响应

解决方案:

function UserProfile({ userId }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    let isActive = true;
    setData(null);
    
    api.get(`/users/${userId}`)
      .then(res => {
        if (isActive) setData(res.data);
      })
      .catch(err => {
        if (isActive) setError(err);
      });
    
    return () => { isActive = false; };
  }, [userId]);
}

请求重试策略

// React Query 指数退避重试
useQuery('todos', fetchTodos, {
  retry: 3,
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
});

// Axios 拦截器重试
api.interceptors.response.use(null, (error) => {
  const config = error.config;
  if (!config || !config.retry) return Promise.reject(error);
  
  config.retryCount = config.retryCount || 0;
  if (config.retryCount >= config.retry) {
    return Promise.reject(error);
  }
  
  config.retryCount += 1;
  return new Promise(resolve => 
    setTimeout(() => resolve(api(config)), config.retryDelay || 1000)
  );
});

大数据量分页优化

// React Query 无限加载
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
  'projects',
  ({ pageParam = 0 }) => api.get(`/projects?offset=${pageParam}&limit=20`),
  {
    getNextPageParam: (lastPage) => 
      lastPage.hasMore ? lastPage.nextOffset : undefined
  }
);

// 虚拟列表渲染
import { FixedSizeList } from 'react-window';

function ProjectList() {
  return (
    <FixedSizeList
      height={600}
      itemCount={data?.pages.flat().length || 0}
      itemSize={100}
      width="100%"
    >
      {({ index, style }) => {
        const item = data.pages.flat()[index];
        return (
          <div style={style}>
            <ProjectItem data={item} />
            {index === data.pages.flat().length - 1 && hasNextPage && (
              <button onClick={fetchNextPage}>加载更多</button>
            )}
          </div>
        );
      }}
    </FixedSizeList>
  );
}

请求取消与垃圾回收

// AbortController 取消请求
useEffect(() => {
  const controller = new AbortController();
  
  const fetchData = async () => {
    try {
      const response = await fetch(url, {
        signal: controller.signal
      });
      // 处理响应
    } catch (err) {
      if (err.name === 'AbortError') {
        console.log('Request aborted');
      } else {
        // 处理其他错误
      }
    }
  };
  
  fetchData();
  
  return () => controller.abort();
}, [url]);

// React Query 自动垃圾回收
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 10 * 60 * 1000 // 10分钟后清理缓存
    }
  }
});

性能优化实践

请求合并与批处理

// GraphQL 请求合并
const GET_USERS = gql`
  query GetUsers($ids: [ID!]!) {
    users(ids: $ids) {
      id
      name
      email
    }
  }
`;

// REST API 批处理中间件
app.use('/api/batch', (req, res) => {
  const requests = req.body;
  Promise.all(requests.map(({ url, method, body }) => 
    handleRequest(url, method, body)
  )).then(results => res.json(results));
});

数据预取策略

// React Router v6.4+ 数据预取
const router = createBrowserRouter([
  {
    path: "/user/:id",
    element: <UserProfile />,
    loader: async ({ params }) => {
      return queryClient.fetchQuery(
        ['user', params.id],
        () => api.get(`/users/${params.id}`)
      );
    }
  }
]);

// 用户悬停预取
<Link 
  to="/user/123"
  onMouseEnter={() => queryClient.prefetchQuery(
    ['user', 123],
    () => api.get('/users/123')
  )}
>
  用户资料
</Link>

离线数据策略

// 使用 IndexedDB 缓存数据
const { data } = useSWR('/api/data', fetcher, {
  onSuccess: (data) => {
    // 保存到 IndexedDB
    idb.set('cachedData', data);
  },
  fallbackData: () => {
    // 从 IndexedDB 获取缓存
    return idb.get('cachedData');
  }
});

// React Query 离线恢复
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst',
      cacheTime: Infinity // 持久化缓存
    }
  }
});

总结

  • 基础方案选择:
    • 简单场景:优先使用 fetch
    • 企业应用:推荐 axios 作为 HTTP 客户端
  • 现代数据管理:
    • 复杂应用:选择 React Query(功能全面)
    • 轻量项目:选择 SWR(快速集成)
  • 关键能力:
    • 智能缓存管理
    • 自动请求重试
    • 乐观更新实现
    • 分页/无限加载
  • 性能优化:
    • 请求批处理
    • 数据预取
    • 离线缓存策略
    • 虚拟列表渲染
  • 面试重点:
    • 请求竞态解决方案
    • 缓存策略设计
    • 错误处理与重试机制
    • 大型数据分页优化

6.5 SSR 与 Next.js 基础

SSR 核心概念

渲染模式对比

渲染方式 特点 SEO 首屏时间 适用场景
客户端渲染 (CSR) 浏览器下载 JS 后渲染 后台系统、Dashboard
服务器端渲染 (SSR) 服务器生成完整 HTML 内容网站、电商
静态站点生成 (SSG) 构建时生成 HTML 极快 博客、文档站
增量静态再生 (ISR) 按需重新生成页面 动态内容站点

SSR 核心优势

  • SEO 优化:搜索引擎可直接抓取完整 HTML 内容
  • 性能提升:减少首屏内容可见时间(FCP)
  • 社交分享优化:社交媒体爬虫获取完整页面内容
  • 低端设备支持:服务器处理渲染工作,降低客户端压力

Next.js 核心架构

Next.js 核心特性

graph TD
    A[Next.js] --> B[路由系统]
    A --> C[渲染策略]
    A --> D[数据获取]
    A --> E[API 路由]
    A --> F[优化功能]
    
    B --> B1[文件系统路由]
    B --> B2[动态路由]
    B --> B3[嵌套路由]
    
    C --> C1[SSG 静态生成]
    C --> C2[SSR 服务器渲染]
    C --> C3[ISR 增量再生]
    
    D --> D1[getStaticProps]
    D --> D2[getServerSideProps]
    D --> D3[getStaticPaths]
    
    E --> E1[无服务器 API]
    E --> E2[中间件支持]
    
    F --> F1[Image 优化]
    F --> F2[字体优化]
    F --> F3[脚本优化]

文件系统路由

pages/
├── index.js          # / 根路由
├── about.js          # /about
├── blog/
│   ├── index.js      # /blog
│   ├── [slug].js     # /blog/:slug 动态路由
│   └── categories/
│       └── [id].js   # /blog/categories/:id 嵌套路由
└── api/
    └── user.js       # /api/user API 路由

数据获取策略

静态生成 (SSG)

// 页面组件
export default function Blog({ posts }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

// 构建时获取数据
export async function getStaticProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  
  return {
    props: { posts },
    revalidate: 60 // 启用 ISR,60秒后重新生成
  };
}

// 动态路由路径生成
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  
  const paths = posts.map(post => ({
    params: { slug: post.slug }
  }));
  
  return {
    paths,
    fallback: 'blocking' // 处理未预生成的路径
  };
}

服务器端渲染 (SSR)

export default function UserProfile({ user }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

// 每次请求时获取数据
export async function getServerSideProps(context) {
  const { req, res, params, query } = context;
  
  // 基于 cookie 认证
  const token = req.cookies.authToken;
  
  const response = await fetch(`https://api.example.com/users/${params.id}`, {
    headers: { Authorization: `Bearer ${token}` }
  });
  
  if (!response.ok) {
    return {
      redirect: {
        destination: '/login',
        permanent: false
      }
    };
  }
  
  const user = await response.json();
  
  return {
    props: { user }
  };
}

高级渲染策略

增量静态再生 (ISR)

export async function getStaticProps() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  
  return {
    props: { products },
    revalidate: 30, // 最多每30秒重新生成一次
  };
}

export async function getStaticPaths() {
  return {
    paths: [
      { params: { category: 'electronics' } }
    ],
    fallback: 'blocking' // 其他类别按需生成
  };
}

按需再验证 (On-demand Revalidation)

// pages/api/revalidate.js
export default async function handler(req, res) {
  // 检查密钥
  if (req.query.secret !== process.env.REVALIDATE_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }
  
  try {
    // 重新验证特定路径
    await res.revalidate('/blog/' + req.query.slug);
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

// 内容更新后调用
// POST /api/revalidate?secret=<token>&slug=new-post

Next.js 高级特性

中间件

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  // 重定向旧路径
  if (request.nextUrl.pathname.startsWith('/old-blog')) {
    return NextResponse.redirect(new URL('/blog', request.url));
  }
  
  // 路径重写
  if (request.nextUrl.pathname === '/dashboard') {
    return NextResponse.rewrite(new URL('/internal-dashboard', request.url));
  }
  
  // 身份验证检查
  const token = request.cookies.get('authToken');
  if (!token && request.nextUrl.pathname.startsWith('/protected')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  return NextResponse.next();
}

// 配置匹配路径
export const config = {
  matcher: ['/dashboard/:path*', '/protected/:path*', '/old-blog/:path*']
};

图像优化

import Image from 'next/image';

function ProductImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}  // 最大显示宽度
      height={600} // 最大显示高度
      placeholder="blur" // 加载占位符
      blurDataURL="data:image/svg+xml;base64,..." // 低质量占位图
      quality={85} // 图像质量 (1-100)
      priority // 关键图像优先加载
      sizes="(max-width: 768px) 100vw, 50vw" // 响应式尺寸
    />
  );
}

API 路由

// pages/api/user/[id].js
export default async function handler(req, res) {
  const { id } = req.query;
  
  switch (req.method) {
    case 'GET':
      const user = await db.user.findUnique({ where: { id } });
      if (!user) return res.status(404).json({ error: 'User not found' });
      return res.status(200).json(user);
    
    case 'PUT':
      const updatedUser = await db.user.update({
        where: { id },
        data: req.body
      });
      return res.status(200).json(updatedUser);
    
    case 'DELETE':
      await db.user.delete({ where: { id } });
      return res.status(204).end();
    
    default:
      res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
      return res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

性能优化实践

代码分割与动态导入

import dynamic from 'next/dynamic';

// 动态加载重组件(禁用 SSR)
const HeavyChart = dynamic(
  () => import('../components/HeavyChart'),
  { ssr: false }
);

// 带加载状态
const MapComponent = dynamic(
  () => import('../components/Map'),
  { 
    loading: () => <div>Loading map...</div>,
    ssr: false
  }
);

// 预加载关键组件
useEffect(() => {
  import('../components/CriticalComponent').then(module => {
    // 组件已加载
  });
}, []);

字体优化

// _document.js
import Document, { Html, Head } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link
            rel="preload"
            href="/fonts/Inter.woff2"
            as="font"
            type="font/woff2"
            crossOrigin="anonymous"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// 全局 CSS 中使用
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap;
}

分析工具集成

// next.config.js
module.exports = {
  experimental: {
    instrumentationHook: true,
  },
};

// instrumentation.js
export function register() {
  if (process.env.NODE_ENV === 'production') {
    import('@vercel/analytics').then(({ inject }) => {
      inject();
    });
    
    const { init } = require('@sentry/nextjs');
    init({
      dsn: process.env.SENTRY_DSN,
      tracesSampleRate: 0.1,
    });
  }
}

面试高频问题

Q:SSR 与 CSR 如何选择?

  • 选择 SSR 当:
    • SEO 是关键需求(内容型网站)
    • 首屏性能至关重要
    • 社交媒体分享需要正确预览
  • 选择 CSR 当:
    • 应用是认证后使用(后台系统)
    • 高度交互式应用(如 Figma)
    • 对服务器成本敏感

Q:getStaticProps vs getServerSideProps

对比:

特性 getStaticProps getServerSideProps
运行时机 构建时 每次请求时
数据新鲜度 静态(可重新验证) 实时动态
性能 极快(CDN 缓存) 较慢(服务器处理)
SEO
使用场景 博客、产品目录 个性化页面、仪表盘

Q:如何处理 SSR 中的身份认证?

解决方案:

// 使用 getServerSideProps 传递用户数据
export async function getServerSideProps(context) {
  const session = await getSession(context);
  
  if (!session) {
    return {
      redirect: { destination: '/login', permanent: false }
    };
  }
  
  return {
    props: { user: session.user }
  };
}

// 客户端使用共享状态
import { useSession } from 'next-auth/react';

function Profile() {
  const { data: session } = useSession();
  
  if (session) {
    return <p>Welcome {session.user.name}</p>;
  }
  
  return <p>Please sign in</p>;
}

Q:如何优化大型 Next.js 应用性能?

优化策略:

  • 代码分割:使用动态导入拆分代码
  • 图像优化:使用 Next Image 组件
  • CDN 缓存:配置静态资源和 ISR 页面缓存
  • 数据库优化:使用缓存层(Redis/Memcached)
  • 减少 Bundle 大小:分析并优化依赖
  • 服务端缓存:对 SSR 结果进行短期缓存
  • 边缘计算:使用 Edge Functions 处理请求

总结

  • SSR 核心价值:
    • 提升 SEO 和首屏性能
    • 改善社交分享体验
    • 支持低端设备用户
  • Next.js 核心能力:
    • 文件系统路由
    • 混合渲染策略(SSG/SSR/ISR)
    • 内置图像优化
    • API 路由支持
  • 关键数据获取方法:
    • getStaticProps:构建时获取数据
    • getServerSideProps:请求时获取数据
    • getStaticPaths:定义动态路由
  • 高级特性:
    • 中间件:请求处理管道
    • 按需重新验证:内容更新后刷新
    • 分析工具:性能监控
  • 面试重点:
    • 渲染策略选择依据
    • 身份认证实现方案
    • 性能优化手段
    • 部署架构设计

第七章:React 工程化与优化

7.1 组件设计模式(复合组件、受控组件模式)

7.2 样式方案(CSS Modules、Styled Components、Tailwind CSS)

7.3 测试(Jest + React Testing Library)

7.4 性能优化(React Profiler、减少重渲染)

7.5 构建工具(Webpack 配置、Vite 优化)

第八章:React 18 新特性与未来趋势

8.1 Concurrent Mode(并发渲染)

8.2 自动批处理(Automatic Batching)

8.3 Transition API(startTransition)

8.4 Server Components(服务端组件)

8.5 React 生态趋势(RSC、React Native)