A05 数据与状态管理:从本地状态到全局数据架构
目标:建立系统化的数据与状态管理思维,能根据业务场景设计合理的数据流、缓存策略和一致性方案。
核心要点(TL;DR)
- 应用中的状态应该被分类管理,不能一刀切地塞进全局 Store。
- 现代前端状态通常分为:本地 UI 状态、全局共享状态、服务端状态、URL 状态、表单状态。
- 服务端状态管理是大型应用的核心挑战,需要处理缓存、失效、乐观更新、去重等问题。
- React Query / SWR / TanStack Query 是服务端状态管理的事实标准。
- 缓存策略没有银弹,需要根据数据特征(读多写少、实时性、一致性要求)选择。
- 数据一致性、冲突解决、离线同步是高级前端架构师必须掌握的能力。
学习时长与前置知识
- 建议学习时长:2-3 周(每周投入 6-8 小时)
- 前置知识:React/Vue、TypeScript、状态管理基础
一、为什么数据与状态管理是架构问题?
1.1 小项目与大项目的分水岭
小项目的状态管理很简单:
js
const [count, setCount] = useState(0);但当项目变大,状态问题会变得复杂:
- 多个组件需要共享同一份数据。
- 同一份数据被多处修改,导致不一致。
- 服务端数据缓存多久?什么时候刷新?
- 用户断网时如何优雅降级?
- 多个用户同时修改同一条数据怎么办?
状态管理是前端架构中最容易出问题的领域之一。
1.2 状态管理不好的症状
如果你的项目出现以下情况,说明状态管理需要重构:
- 改了 A 组件的状态,B 组件没更新。
- 页面刷新后,数据需要重新加载,体验差。
- 同一个接口被多个组件重复请求。
- 表单提交后,列表没有自动刷新。
- 状态逻辑分散在组件里,难以测试和维护。
1.3 生活化比喻
状态管理像城市的水电气系统:
- 本地状态是你家里的开关,只影响一个房间。
- 全局状态是小区的总闸,影响多个住户。
- 服务端状态是城市供水厂,源头在水厂,家里只是缓存。
- URL 状态是门牌号,告诉外卖员你住哪。
- 缓存策略是水箱容量和换水频率,太小不够用,太大不新鲜。
好的状态管理,就是让每个状态都在最合适的位置,用最合适的方式流动。
二、状态的分类
2.1 五类状态
| 状态类型 | 说明 | 示例 | 管理方式 |
|---|---|---|---|
| 本地 UI 状态 | 只影响单个组件或局部界面 | 弹窗开关、加载状态、表单输入临时值 | useState / useReducer |
| 全局共享状态 | 多个组件共享 | 用户信息、主题、权限 | Zustand / Redux / Pinia |
| 服务端状态 | 来自服务端的数据 | 用户列表、订单详情、商品数据 | React Query / SWR / TanStack Query |
| URL 状态 | 存储在 URL 中的状态 | 当前页码、筛选条件、选中标签 | 路由参数 / Query String |
| 表单状态 | 表单相关的复杂状态 | 字段值、校验错误、提交状态 | React Hook Form / Formik |
2.2 为什么需要分类?
不同状态有不同的生命周期、更新来源和一致性要求:
- 本地 UI 状态不需要持久化,不需要共享。
- 全局共享状态需要跨组件同步,但不需要持久化。
- 服务端状态的“真相”在服务端,前端只是缓存,需要考虑缓存策略。
- URL 状态可以分享和回退,适合做页面级状态。
- 表单状态有复杂的校验和提交逻辑,需要专门管理。
常见反模式:把所有状态都放到 Redux,导致 Store 臃肿、性能差、开发体验差。
三、客户端状态管理
3.1 useState / useReducer
适合本地和局部状态:
tsx
// 简单状态
const [isOpen, setIsOpen] = useState(false);
// 复杂状态用 useReducer
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: return state;
}
}3.2 Context API
适合跨组件传递“低频更新”的状态,如主题、语言、用户信息。
tsx
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Child />
</ThemeContext.Provider>
);
}注意:Context 不适合高频更新的状态,因为会导致大量组件重渲染。
3.3 全局状态管理库
| 库 | 特点 | 适用场景 |
|---|---|---|
| Redux | 生态成熟、可预测、严格 | 超大型应用、需要强约束 |
| Zustand | 简单、轻量、灵活 | 中小型应用、快速开发 |
| MobX | 响应式、面向对象 | 适合 OOP 背景团队 |
| Pinia | Vue 官方推荐 | Vue 项目 |
| Jotai / Recoil | 原子化状态 | React 项目,细粒度订阅 |
3.4 选型建议
- 小项目:useState + Context 足够。
- 中大型 React 项目:Zustand + React Query 是黄金组合。
- 需要强审计和调试:Redux Toolkit。
- Vue 项目:Pinia。
四、服务端状态管理
4.1 服务端状态的特殊性
服务端状态与客户端状态最大的区别:
- 来源不同:服务端状态的真相在服务端。
- 需要异步获取:有 loading、error、success 等状态。
- 需要缓存:避免重复请求。
- 需要失效策略:数据过期后如何刷新。
- 需要乐观更新:提升用户体验。
- 需要去重:多个组件同时请求同一个接口,只发一次请求。
4.2 React Query / SWR / TanStack Query
这些库是服务端状态管理的事实标准。
核心概念:
| 概念 | 说明 |
|---|---|
| Query | 读取服务端数据 |
| Mutation | 修改服务端数据 |
| Cache Key | 缓存的唯一标识 |
| Stale Time | 数据被视为“新鲜”的时间 |
| Cache Time | 数据在缓存中保留的时间 |
| Refetch | 重新获取数据 |
| Invalidation | 让缓存失效 |
| Optimistic Update | 乐观更新 |
4.3 React Query 示例
tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 读取数据
function useUser(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
});
}
// 修改数据
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data) => fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }),
onSuccess: () => {
// 让 user 相关缓存失效
queryClient.invalidateQueries({ queryKey: ['user'] });
}
});
}4.4 服务端状态设计原则
- 每个服务端数据都有唯一的 Query Key。
- 合理设置 staleTime 和 cacheTime:读多写少的数据可以缓存久一点。
- Mutation 后主动失效相关 Query。
- 乐观更新要提供回滚机制。
- 错误处理要统一:重试、降级、提示。
五、缓存策略
5.1 缓存的位置
用户 → 浏览器缓存 → CDN 缓存 → Edge 缓存 → BFF 缓存 → 数据库缓存 → 数据库前端主要关注:
- HTTP 缓存:Cache-Control、ETag、Last-Modified。
- 内存缓存:React Query / SWR 的内存缓存。
- 本地存储:localStorage、IndexedDB、SessionStorage。
5.2 HTTP 缓存策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache-Control: no-cache | 每次都要向服务端验证 | 频繁变化的数据 |
| Cache-Control: no-store | 完全不缓存 | 敏感数据 |
| Cache-Control: max-age=3600 | 缓存 1 小时 | 静态资源、变化不频繁的数据 |
| ETag + If-None-Match | 协商缓存 | 需要验证新鲜度的资源 |
5.3 前端数据缓存策略
| 策略 | 说明 | 示例 |
|---|---|---|
| Stale-While-Revalidate | 先返回缓存,后台更新 | React Query 默认策略 |
| Cache First | 优先缓存,没有再走网络 | 离线应用 |
| Network First | 优先网络,失败用缓存 | 实时性要求高的数据 |
| Time-Based | 按时间过期 | 5 分钟刷新一次 |
| Event-Based | 特定事件触发刷新 | 用户操作后刷新列表 |
5.4 缓存失效策略
缓存最难的问题是:什么时候让缓存失效。
常见策略:
- 时间过期:最简单,但可能数据不新鲜。
- 主动失效:Mutation 后手动 invalidate。
- 事件驱动:WebSocket / SSE 推送数据变化。
- 版本号:资源 URL 带版本号,如
app.v2.js。 - ETag 协商:服务端决定资源是否变化。
六、数据一致性
6.1 一致性模型
| 模型 | 说明 | 适用场景 |
|---|---|---|
| 强一致性 | 任何时刻所有节点数据一致 | 金融交易、库存扣减 |
| 最终一致性 | 允许短暂不一致,最终一致 | 社交网络、内容展示 |
| 因果一致性 | 有因果关系的事件顺序一致 | 聊天应用、协作编辑 |
6.2 前端常见的一致性问题
问题 1:列表和详情数据不一致
用户修改了订单详情,回到列表发现数据还是旧的。
解决:
- Mutation 成功后 invalidate 列表和详情的 Query。
- 或手动更新缓存中的列表项。
问题 2:多个标签页数据不一致
用户在标签页 A 修改了数据,标签页 B 还是旧数据。
解决:
- 使用 Broadcast Channel 同步状态。
- 页面重新获得焦点时 refetch。
问题 3:离线后重新上线
用户在离线时做了修改,重新联网后如何同步?
解决:
- 本地队列存储未同步的操作。
- 联网后按顺序执行,处理冲突。
6.3 乐观更新与回滚
乐观更新假设操作会成功,先更新 UI,再发送请求:
tsx
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 取消正在进行的 refetch
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存之前的值
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], old => [...old, newTodo]);
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 出错时回滚
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// 无论成功与否,最终 refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});6.4 冲突解决策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Last Write Wins | 以最后一次写入为准 | 简单场景 |
| Version Vector | 根据版本号判断冲突 | 协作编辑 |
| Operational Transformation | 操作转换 | 在线文档 |
| CRDT | 无冲突复制数据类型 | 离线优先应用 |
七、URL 状态与表单状态
7.1 URL 状态
URL 是天然的“可分享状态存储”:
/products?category=electronics&page=2&sort=price_asc优点:
- 可分享、可回退、可刷新。
- 不需要额外状态管理。
适合场景:
- 列表页筛选、排序、分页。
- 详情页选中项。
- 搜索条件。
7.2 表单状态
表单是前端最复杂的状态之一,涉及:
- 字段值
- 校验状态
- 错误信息
- 提交状态
- 字段联动
推荐使用专门的表单库:
| 库 | 特点 |
|---|---|
| React Hook Form | 性能优秀、基于非受控组件 |
| Formik | API 直观、生态丰富 |
| Ant Design Form | 与 AntD 深度集成 |
八、最佳实践
- 按状态类型选择管理工具:不要把所有状态塞进一个 Store。
- 服务端状态用专用库:React Query / SWR / TanStack Query。
- 全局状态保持最小:只放真正需要全局共享的状态。
- URL 能表达的状态放 URL:筛选、分页、排序。
- 合理设置缓存策略:读多写少缓存久,实时性要求高缓存短。
- Mutation 后主动失效相关缓存。
- 乐观更新必须能回滚。
- 关注离线场景和一致性:尤其是移动端和弱网环境。
- 避免 prop drilling:用 Context 或状态管理库解决。
- 状态逻辑要可测试:把业务逻辑从组件中抽离。
九、总结
数据与状态管理是前端架构的核心能力:
- 初级:会用 useState、Context、Redux 管理状态。
- 中级:能区分五类状态,合理使用 React Query/SWR。
- 高级:能设计复杂应用的数据流、缓存策略和一致性方案。
- 架构师:能把状态管理标准化为团队规范,搭建可复用的数据层。
好的状态管理让应用可预测、可维护、可扩展;差的状态管理会让代码迅速失控。
十、延伸阅读与资源
必读文章
- 🟢 React Query 官方文档
- 🟡 State Management in React — React 官方。
- 🟡 The Rise of Server State — React Query 维护者博客。
书籍
- 🟡 《Redux 实战》
- 🔴 《Designing Data-Intensive Applications》— Martin Kleppmann
实践项目
- 用 React Query 重构一个现有项目的数据层。
- 实现一个支持离线编辑的 Todo 应用(乐观更新 + 冲突解决)。
- 设计一个大型列表页的 URL 状态管理方案。
领域编号:A05 数据与状态管理
最后更新:2026-06-18
本领域学习进度
学习进度0 / 43 (0%)