前端状态管理深度笔记
覆盖主流状态管理方案的核心原理、实现对比、最佳实践,以及服务端/客户端状态分离、状态规范化等通用原则。
1. 方案对比总览
| 方案 | 核心思想 | 数据结构 | 不可变 | 学习曲线 | 体积 | TS 支持 | 适用场景 |
|---|---|---|---|---|---|---|---|
| Redux (RTK) | 单一 Store、Flux 单向流 | 纯对象(reducer 返回新对象) | 是(immer 辅助) | 中高 | ~12KB | 极好 | 大型应用、复杂异步逻辑 |
| MobX | 可观察状态、自动追踪 | 可变对象(Proxy/defineProperty) | 否 | 低 | ~16KB | 好 | 状态间依赖复杂的中型应用 |
| Zustand | 小型独立 Store、Hook 驱动 | 纯对象 + 不可变更新 | 是(约定) | 极低 | ~1KB | 极好 | 中/小型应用、组件状态提升 |
| Jotai | 原子化状态、Bottom-up | 原子(Atom,独立状态单元) | 是(不可变原子) | 低 | ~3KB | 好 | 需要细粒度订阅的场景 |
| Pinia | 去中心化 Store、组合式 API | 响应式对象(Vue reactive) | 否(可变修改) | 低 | ~1KB | 极好 | Vue 3 应用 |
| Vuex | 单一 Store、Mutation/Action 分离 | 响应式对象 | 否(可变修改) | 中 | ~5KB | 一般 | Vue 2 遗留项目 |
核心选择原则
应用规模小且状态简单 → useState / ref + Context / provide-inject
应用规模中等 → Zustand / Jotai / Pinia
应用规模大且异步逻辑复杂 → Redux Toolkit / RTK Query
Vue 3 项目 → Pinia
需要细粒度订阅 → Jotai(React)/ Pinia(Vue)
不可变性偏好 → Redux / Zustand
可变性偏好 → MobX
2. Flux 模式
2.1 单向数据流
┌──────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐
│ Action │ ───→ │ Dispatcher │ ───→ │ Store │ ───→ │ View │
│ (描述变更)│ │ (调度中心) │ │ (状态+逻辑)│ │ (UI组件) │
└──────────┘ └────────────┘ └─────────┘ └─────────┘
↑ │
└─────────────────────── 用户交互 ────────────────────────┘
核心特点:
- 单向:数据只能沿一个方向流动(Action → Dispatcher → Store → View)
- 确定性:同一 Action 序列产生相同的状态变化
- 可追踪:所有状态变更都通过 Action 触发,便于调试(时间旅行)
2.2 Flux vs MVC 在大型应用中的区别
MVC 在大型应用中容易出现双向数据流混乱(Model 更新 View,View 事件又修改 Model),Flux 通过强制单向流解决了这个问题。
2.3 Redux 是对 Flux 的演进
Redux 简化了 Flux:
- 单一 Store:vs Flux 的多个 Store
- 无 Dispatcher:reducer 本身就是纯函数调度
- 纯函数 Reducer:
(prevState, action) => newState
3. Redux Toolkit
3.1 createSlice(immer 不可变更新)
import { createSlice } from '@reduxjs/toolkit'
const postsSlice = createSlice({
name: 'posts',
initialState: {
items: [],
status: 'idle',
error: null
},
reducers: {
// immer 代理 state,可以直接"修改"
addPost: (state, action: PayloadAction<Post>) => {
state.items.push(action.payload) // 看起来是 mutable 操作
},
deletePost: (state, action) => {
state.items = state.items.filter(p => p.id !== action.payload)
},
resetPosts: () => ({ items: [], status: 'idle', error: null })
}
})
// 自动生成 actions: postsSlice.actions.addPost
immer 原理:produce(baseState, recipe) 用 Proxy 拦截 draft 上的所有修改操作,记录变更成 patch,最后根据 patch 生成新对象(不可变)。
3.2 createAsyncThunk(异步状态管理)
const fetchPosts = createAsyncThunk(
'posts/fetchPosts',
async (_, { rejectWithValue }) => {
try {
const res = await fetch('/api/posts')
if (!res.ok) return rejectWithValue('Fetch failed')
return await res.json()
} catch (err) {
return rejectWithValue(err.message)
}
}
)
const postsSlice = createSlice({
name: 'posts',
initialState: { items: [], status: 'idle', error: null },
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = 'loading'
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'
state.items = action.payload
})
.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'
state.error = action.payload
})
}
})
内部原理:createAsyncThunk 生成三个 action types(pending/fulfilled/rejected),返回的 thunk action 内部 dispatch 这三个 action。
3.3 RTK Query(缓存策略)
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Posts', 'User'],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => 'posts',
// 缓存策略
keepUnusedDataFor: 60, // 缓存保留时间(秒)
providesTags: ['Posts'], // 提供哪些标签
}),
addPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({
url: 'posts',
method: 'POST',
body,
}),
// 自动失效缓存
invalidatesTags: ['Posts'],
}),
// 乐观更新
updatePost: builder.mutation<Post, Post>({
query: ({ id, ...patch }) => ({
url: `posts/${id}`,
method: 'PATCH',
body: patch,
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// 乐观更新:先更新 UI
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === id)
if (post) Object.assign(post, patch)
})
)
try {
await queryFulfilled
} catch {
// 请求失败,回滚
patchResult.undo()
}
}
})
})
})
RTK Query 缓存机制:
- 每个查询结果缓存 + tag 标记
- Mutation 通过
invalidatesTags使缓存过期 - 订阅该 tag 的 query 自动重新获取
keepUnusedDataFor控制缓存保留时间- 提供
matchFulfilled等监听器实现响应式更新
3.4 Middleware 原理
Redux Middleware 是柯里化函数,在 dispatch 和 reducer 之间增加一层:
// Middleware 签名
const middleware = (store) => (next) => (action) => {
console.log('dispatching', action)
const result = next(action) // 传给下一个 middleware 或 reducer
console.log('next state', store.getState())
return result
}
链式调用:applyMiddleware(m1, m2, m3) 将 middleware 组合成链,dispatch 穿透时依次执行。
常见中间件:
redux-thunk— 允许 dispatch 函数(thunk),处理异步redux-saga— 基于 Generator 的副作用管理redux-observable— 基于 RxJS 的异步流处理- RTK 内置
createAsyncThunk替代 thunk/saga 的大部分场景
4. Context 方案
4.1 React Context 的局限性
const AppContext = createContext({ user: null, theme: 'light', cart: [] })
// ❌ 任何值变化触发所有 Consumer 重渲染
// 即使 ThemeProvider 的 theme 从 'light' → 'dark'
// 只用了 user 的 Profile 组件也会重新渲染
核心缺陷:Context 没有选择器机制,值变化时所有 Consumer 不论是否需要都重渲染。
4.2 拆分 Context 的策略
const UserContext = createContext(null)
const ThemeContext = createContext('light')
const CartContext = createContext([])
function App() {
return (
<UserProvider>
<ThemeProvider>
<CartProvider>
<Main />
</CartProvider>
</ThemeProvider>
</UserProvider>
)
}
何时拆分:不同数据更新频率差异大时必须拆分。如:用户偏好(低频)与购物车(高频)分开。
4.3 use-context-selector 原理
import { createContext, useContextSelector } from 'use-context-selector'
const Context = createContext(null)
function UserName() {
// 只订阅 user.name 的变化
const name = useContextSelector(Context, (state) => state.user.name)
return <div>{name}</div>
}
原理:内部维护每个 Context 的订阅者列表 + selector 函数列表。Provider 更新时遍历订阅者,执行 selector 比较新旧值,只有 selector 返回值变化时才触发重新渲染。
5. Zustand
5.1 create 函数原理
// Zustand 的核心实现(约 200 行)
import { useSyncExternalStore } from 'react'
function createStore(createState) {
let state
const listeners = new Set<() => void>()
const setState = (partial, replace = false) => {
const nextState = typeof partial === 'function'
? partial(state)
: partial
if (nextState !== state) {
const previousState = state
state = replace ? nextState : Object.assign({}, state, nextState)
listeners.forEach(listener => listener())
}
}
const getState = () => state
const subscribe = (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
}
const destroy = () => listeners.clear()
state = createState(setState, getState, api)
const api = { setState, getState, subscribe, destroy }
return api
}
// React Hook 绑定(通过 useSyncExternalStore)
export function create<State>(createState) {
const api = createStore(createState)
const useStore = (selector = identity, equalityFn = Object.is) =>
useSyncExternalStoreWithSelector(
api.subscribe,
api.getState,
api.getServerState,
selector,
equalityFn
)
Object.assign(useStore, api)
return useStore
}
核心:create 返回一个既是 Hook 又是 Store API 的函数。Hook 内部通过 useSyncExternalStore 订阅 store,使得 Zustand 不依赖 React Context。
5.2 不可变更新与 immer 中间件
// 直接不可变更新
const useStore = create((set) => ({
todos: [],
addTodo: (todo) => set((state) => ({
todos: [...state.todos, todo] // 手动不可变
}))
}))
// immer 中间件("可变"写法)
import { immer } from 'zustand/middleware/immer'
const useStore = create(
immer((set) => ({
todos: [],
addTodo: (todo) => set((state) => {
state.todos.push(todo) // immer 代理,实际不可变
})
}))
)
5.3 订阅选择器(selector + shallow)
// 精确订阅 —— 只有 bears 变化时重渲染
const bears = useBearStore(s => s.bears)
// 浅比较订阅 —— 返回的对象的顶层属性变化时重渲染
import { shallow } from 'zustand/shallow'
const { name, age } = useBearStore(
s => ({ name: s.name, age: s.age }),
shallow // 默认是 strict equal (===),shallow 做浅比较
)
5.4 多个 Store 的设计模式
// 按业务领域拆分 Store
const useUserStore = create(...)
const useCartStore = create(...)
const useUIStore = create(...)
// Store 间可互相访问(如下单时读取用户信息)
const useOrderStore = create((set, get) => ({
placeOrder: async () => {
const user = useUserStore.getState().user
const cart = useCartStore.getState().items
const order = await api.placeOrder(user.id, cart)
set({ order })
useCartStore.getState().clearCart() // 下单后清空购物车
}
}))
模式原则:按数据更新频率和业务领域拆分,避免"上帝 Store"。
6. Jotai / VueUse
6.1 原子化状态
import { atom, useAtom } from 'jotai'
// 定义原子
const countAtom = atom(0)
const doubledAtom = atom((get) => get(countAtom) * 2)
const asyncDataAtom = atom(async () => {
const res = await fetch('/api/data')
return res.json()
})
// 组件中使用
function Counter() {
const [count, setCount] = useAtom(countAtom)
const [doubled] = useAtom(doubledAtom)
return <div>{count} x 2 = {doubled}</div>
}
6.2 派生状态
// 派生原子(只读)
const isHighAtom = atom((get) => get(countAtom) > 100)
// 写原子(读写)
const toggleAtom = atom(
(get) => get(countAtom),
(get, set, newValue: number) => {
set(countAtom, newValue)
set(logAtom, [...get(logAtom), newValue])
}
)
原理:原子构成依赖图(DAG),Jotai 内部按拓扑序更新,每个原子只重新计算受影响的派生原子。
6.3 异步原子
const userAtom = atom(async (get) => {
const userId = get(userIdAtom)
const res = await fetch(`/api/users/${userId}`)
return res.json()
})
// 使用 Suspense 或 loadable
function User() {
return (
<Suspense fallback={<Loading />}>
<UserContent />
</Suspense>
)
}
// 或使用 loadable 避免 Suspense
const userLoadable = useAtomValue(loadable(userAtom))
if (userLoadable.state === 'loading') return <Loading />
6.4 Jotai 与 Zustand 对比
| 对比维度 | Zustand | Jotai |
|---|---|---|
| 抽象层级 | Store(全局状态容器) | Atom(原子化状态单元) |
| 细粒度订阅 | 需要 selector | 天然细粒度(每个 atom 独立) |
| 派生状态 | set/get 时手动计算 | 自动推导(依赖图) |
| 与 React Context | 不依赖 | 底层的 Provider 仍用 Context |
| 异步支持 | 手动管理 | 原生支持异步 atom |
| 心智模型 | 集中式 Store | 分散式原子 |
7. 通用原则
7.1 服务端状态 vs 客户端状态分离
┌─────────────────────────────────────────────┐
│ 服务端状态(Server State) │
│ - 存储在服务端数据库 │
│ - 数据获取(fetch/query) │
│ - 缓存策略(stale-while-revalidate) │
│ - 可能被其他用户/设备修改 │
│ - 示例:文章列表、用户信息、订单数据 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 客户端状态(Client State) │
│ - 仅存在于客户端 │
│ - 无需网络请求 │
│ - 刷新即丢失(除非持久化) │
│ - 示例:UI 状态(折叠/展开)、表单输入、主题 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ URL 状态(URL State) │
│ - 存在于 URL(路径/查询参数/哈希) │
│ - 可分享、可收藏、可后退/前进 │
│ - 示例:当前页面、搜索关键词、筛选条件 │
└─────────────────────────────────────────────┘
分离的好处:
- 服务端状态用专用工具管理(RTK Query / TanStack Query / SWR)
- 客户端状态用通用工具管理(Zustand / Context / useState)
- 避免将服务端数据缓存在全局 Store 中做手动同步
7.2 状态放置位置的优先级
第一选择
↓
┌──────┐
│ URL │ ← 可分享、可书签、可导航
└──────┘
↓
┌──────────┐
│ 全局 Store │ ← 多组件共享、跨页面状态
└──────────┘
↓
┌────────────┐
│ 组件提升状态 │ ← 父子或兄弟间共享
└────────────┘
↓
┌──────────────┐
│ 组件局部状态 │ ← 仅当前组件需要
└──────────────┘
↓
┌──────────────┐
│ 局部变量/ref │ ← 不需要触发渲染的值
└──────────────┘
↓
最后选择
判断步骤:
- 这个状态是否应出现在 URL 中?(搜索、筛选、分页)→ URL State
- 是否被多层或远离的组件共享?→ 全局 Store
- 是否只是父子间共享?→ Lifting State Up
- 是否只属于一个组件?→ useState/useReducer
- 是否不需要触发 UI 更新?→ useRef / 局部变量
7.3 状态规范化(Normalization)
问题:嵌套对象在 Store 中导致数据冗余、更新困难、同步不一致。
❌ 非规范化
{
posts: [
{ id: 1, title: "...", author: { id: 1, name: "Alice" }, comments: [...] }
]
}
✅ 规范化(类似数据库设计)
{
posts: {
byId: { "1": { id: 1, title: "...", authorId: 1, commentIds: ["100"] } },
allIds: ["1"]
},
users: {
byId: { "1": { id: 1, name: "Alice" } },
allIds: ["1"]
},
comments: {
byId: { "100": { id: "100", text: "..." } },
allIds: ["100"]
}
}
好处:
- 单一数据源(用户信息只存一次)
- 更新用户信息只需改一处
- 减少冗余数据,降低 Store 体积
- 更高效的选择和查找
工具:Redux 推荐 normalizr 库,RTK Query 原生支持规范化的缓存。
7.4 状态持久化
// Zustand persist 中间件
import { persist } from 'zustand/middleware'
const useStore = create(
persist(
(set) => ({
count: 0,
increment: () => set(s => ({ count: s.count + 1 })),
}),
{
name: 'app-storage', // localStorage key
partialize: (state) => ({ count: state.count }), // 只持久化部分字段
merge: (persisted, current) => ({
...current,
...(persisted as Partial<typeof current>),
// 版本迁移
}),
onRehydrateStorage: () => (state) => {
// 恢复完成后的回调
}
}
)
)
持久化的注意事项:
- 只持久化必要的状态(排除计算属性、临时 UI 状态)
- 处理持久化后的版本兼容(版本迁移策略)
- 考虑敏感数据(token)的安全存储
- 大对象考虑压缩或 IndexedDB
7.5 乐观更新(Optimistic Update)
思想:用户操作后立即更新 UI,不等待服务端响应。
// 乐观更新的通用模式
async function toggleLike(postId: string) {
// 1. 保存原始状态(用于回滚)
const previousPosts = store.getState().posts
// 2. 乐观更新 UI
store.setState(s => ({
posts: s.posts.map(p =>
p.id === postId
? { ...p, liked: !p.liked, likes: p.liked ? p.likes - 1 : p.likes + 1 }
: p
)
}))
try {
// 3. 发送实际请求
await api.toggleLike(postId)
} catch {
// 4. 失败时回滚
store.setState({ posts: previousPosts })
}
}
适用场景:点赞、收藏、发送消息等高频且大概率成功的操作。
不适用:支付、订单提交等需要服务端确认的关键操作。
7.6 订阅选择器性能优化
组件 A: 使用整个 store
每次变化重新渲染(最差)
组件 B: 使用 selector 选择部分状态
s => s.user.name
仅 name 变化渲染(好)
组件 C: 返回新对象的 selector + shallow 比较
s => ({ a: s.a, b: s.b })
shallow 比较顶层属性(较好)
组件 D: 返回新对象的 selector(无 shallow)
s => ({ a: s.a, b: s.b })
每次生成新对象 → 每次都渲染(差)
结论:
- selector 尽量返回基础类型
- 返回对象时配合
shallow或useShallow - 避免 selector 中创建新数组/对象(用
createSelector缓存)