专题知识学习: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 协调机制处理。这带来三方面优势:
- 可维护性:状态变化自动触发 UI 更新,避免手动 DOM 操作导致的逻辑分散
- 性能优化:React 通过 Diff 算法批量 DOM 更新,减少重绘开销
- 开发体验:更接近自然思维模式的 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>
// 渲染为:<script>alert(1)</script>
例外: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>
);
}
最佳实践总结
- 纯函数原则:Reducer 必须是纯函数,不产生副作用
- 不可变更新:始终返回新状态对象,不修改原状态
- 类型安全:使用 TypeScript 强化 action 类型
type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'set_count'; payload: number }; - 逻辑复用:提取 reducer 到独立文件便于测试
- 合理拆分:大型应用按功能域拆分多个 reducer
- 避免过度:简单状态仍使用 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 设计原则
- 单一职责:一个 Hook 只解决一个问题
- 命名清晰:use + 功能名(useWindowSize)
- 参数合理:提供默认值和必要配置
- 返回简洁:返回数组或对象结构
- 文档完善:使用 JSDoc 说明用法
- 类型安全: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; }
性能优化原则总结
- 测量优先:优化前先使用 Profiler 分析性能瓶颈
- 避免过早优化:只在必要时应用优化技术
- 关注关键路径:优先优化渲染频率高的组件
- 合理使用工具:
- React.memo 用于组件记忆化
- useMemo 用于昂贵计算缓存
- useCallback 用于函数引用稳定
- 结构优化:
- 组件职责单一化
- 状态合理下沉
- 避免深层嵌套
面试高频问题与解答
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="关闭">
×
</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="关闭通知">
×
</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 纯函数 |
| 状态共享范围 | 父组件及子组件 | 全局可访问 |
| 中间件支持 | 无 | 支持异步操作 |
最佳实践总结
- 最小状态原则:只提升必要共享的状态
- 保持状态局部:非共享状态保持在组件内部
- 避免过度提升:超过3层传递考虑 Context
- 数据不可变:状态更新时创建新对象
// ❌ 错误 formData.items.push(newItem); setFormData(formData); // ✅ 正确 setFormData(prev => ({ ...prev, items: [...prev.items, newItem] })); - 回调函数命名:统一使用 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 三大原则
- 单一数据源 (Single Source of Truth)
// 整个应用只有一个Store console.log(store.getState()); /* 输出: { todos: [], visibilityFilter: 'SHOW_ALL' } */ - 状态只读 (State is Read-Only)
// ❌ 禁止直接修改 store.getState().todos.push('Illegal mutation!'); // ✅ 唯一修改方式:dispatch(action) store.dispatch({ type: 'ADD_TODO', text: 'Legal update' }); - 纯函数修改 (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是纯函数?
- 可预测性:相同输入必定得到相同输出
- 时间旅行:支持状态快照和回滚
- 易于测试:无需模拟环境,直接测试输入输出
- 性能优化:可安全进行浅比较
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) { ... }
状态设计原则
- 扁平化结构:避免嵌套过深
- 按业务域拆分:不同功能独立状态分支
- 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:为什么需要中间件处理异步?
- Redux 自身限制:reducer 必须是纯函数,不能包含副作用
- 可预测性:集中管理副作用,避免分散在组件中
- 可维护性:统一处理错误、日志、重试等通用逻辑
- 高级功能:实现请求取消、竞态处理等复杂场景
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?
- 解耦组件:避免组件直接依赖 Redux store
- 性能优化:自动实现 shouldComponentUpdate 优化
- 上下文管理:通过 Provider 优雅注入 store
- 简化开发:提供 useSelector/useDispatch 便捷 API
- 未来兼容:官方维护,保证与 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 }); // 获取当前最新值 } }));
- 错误示例 (Zustand 闭包陷阱):
- 与 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> - 自动选择最佳匹配的路由(不再按顺序匹配)
- 路由匹配容器,替代 v5 的
<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:定义动态路由
- 高级特性:
- 中间件:请求处理管道
- 按需重新验证:内容更新后刷新
- 分析工具:性能监控
- 面试重点:
- 渲染策略选择依据
- 身份认证实现方案
- 性能优化手段
- 部署架构设计