引言:前端状态管理的三十年战争
前端状态管理是一个永不过时的话题。从早期 jQuery 时代的手动 DOM 操作,到 MVC 框架的兴起,再到 Flux 架构的提出和 Redux 的一统天下,最后到今天百花齐放的 Zustand、Jotai、Signal 等轻量方案,这个领域的演进史本质上就是前端工程化程度不断加深的缩影。
站在 2026 年回望,我们不禁要问:为什么需要这么多状态管理方案?它们各自的优劣是什么?在实际项目中应该如何选择?本文将带你深入剖析从 Redux 到现代轻量方案的完整演进路径,并通过实战代码展示各自的适用场景。
无论你是刚接触 React/ Vue 生态的新手,还是已经用过多个框架的老手,这篇文章都能帮你建立完整的状态管理知识图谱。
Redux:奠定范式的一代宗师
Redux 诞生于 2015 年,由 Dan Abramov 和 Andrew Clark 创造。它的核心思想源于 Flux 架构和 Elm 语言,提出了三个基本原则:单一数据源(Single Source of Truth)、状态只读(State is Read-Only)、纯函数修改(Changes are Made with Pure Functions)。
一个典型的 Redux 代码如下:
1 // Action Types const ADD_TODO = 'ADD_TODO'; const TOGGLE_TODO = 'TOGGLE_TODO'; // Action Creators const addTodo = (text) => ({ type: ADD_TODO, payload: { id: Date.now(), text, completed: false } }); const toggleTodo = (id) => ({ type: TOGGLE_TODO, payload: { id } }); // Reducer const todosReducer = (state = [], action) => { switch (action.type) { case ADD_TODO: return [...state, action.payload]; case TOGGLE_TODO: return state.map(todo => todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo ); default: return state; } }; // Store const store = createStore(todosReducer);
Redux 最大的贡献在于为前端状态管理建立了 可预测性(Predictability)。因为 reducer 是纯函数,给定相同的输入永远返回相同的输出,这让调试和测试变得异常简单。配合 Redux DevTools,你可以时光回溯(Time Travel Debugging),回到任意一个历史状态。
然而 Redux 的问题也同样突出:样板代码过多(Boilerplate Overload)。一个简单的 TODO 功能就要写 action types、action creators、reducer、以及 connect 或 useSelector 的组件层代码。社区为了缓解这个问题推出了 Redux Toolkit(RTK),内嵌了 Immer 来实现可变状的不可变更新,并提供了 createSlice 大幅简化样板:
1 import { createSlice, configureStore } from '@reduxjs/toolkit'; const todosSlice = createSlice({ name: 'todos', initialState: [], reducers: { addTodo: { reducer(state, action) { state.push(action.payload); }, prepare(text) { return { payload: { id: Date.now(), text, completed: false } }; } }, toggleTodo(state, action) { const todo = state.find(t => t.id === action.payload); if (todo) todo.completed = !todo.completed; } } }); const store = configureStore({ reducer: { todos: todosSlice.reducer } });
但即使有 RTK,Redux 的整体心智模型仍然较重:你需要在 Action → Dispatch → Reducer → Store → Subscribe 这条链上理解每一个环节。对于中小型项目来说,这种复杂度往往得不偿失。
Zustand:极简主义的胜利
Zustand(德语中”状态”的意思)由 Jotai 的作者 Daishi Kato 参与开发,发布于 2019 年。它的设计哲学是 “最小 API 表面(Minimal API Surface)”——你不需要 Provider 包裹、不需要 action types、不需要 reducer,只需要一个 hook。
同样的 TODO 功能,用 Zustand 实现:
1 import { create } from 'zustand'; const useTodoStore = create((set) => ({ todos: [], addTodo: (text) => set((state) => ({ todos: [...state.todos, { id: Date.now(), text, completed: false }] })), toggleTodo: (id) => set((state) => ({ todos: state.todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t ) })), getCompletedCount: () => { // 注意:getState 可以在任何地方使用 return useTodoStore.getState().todos.filter(t => t.completed).length; } })); // 在组件中使用 function TodoList() { const { todos, addTodo, toggleTodo } = useTodoStore(); // ... }
Zustand 的杀手锏在于:
- 零 Provider:不需要包裹整个应用,store 是全局单例,但可以按需创建多个 store。
- 精细订阅:只选择你需要的状态,自动跳过不必要的重渲染。
- 不绑定 React:Zustand v4+ 的核心逻辑完全与框架无关,可以原生在 Vue、Angular 甚至纯 JS 中使用。
- 中间件系统:内置 persist、immer、devtools、subscribeWithSelector 等中间件。
来看一个更复杂的实际案例——带持久化和异步操作的状态管理:
1 import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; interface UserState { user: User | null; loading: boolean; error: string | null; login: (username: string, password: string) => Promise<void>; logout: () => void; } const useUserStore = create<UserState>()( persist( immer((set) => ({ user: null, loading: false, error: null, login: async (username, password) => { set((state) => { state.loading = true; state.error = null; }); try { const res = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ username, password }) }); if (!res.ok) throw new Error('Login failed'); const user = await res.json(); set((state) => { state.user = user; state.loading = false; }); } catch (err) { set((state) => { state.error = err.message; state.loading = false; }); } }, logout: () => set((state) => { state.user = null; }) })), { name: 'user-storage' } // 自动持久化到 localStorage ) );
Zustand 的局限性主要在于 原子级订阅不够灵活——虽然可以通过选择器控制,但 selector 返回的对象如果每次都创建新引用,会触发不必要的重渲染。对此,Zustand 提供了
1 | useShallow |
来解决。
Jotai:原子状态的哲学
Jotai(日语”状态”的意思)同样由 Daishi Kato 创作,但采用了完全不同的思路——原子模型(Atomic Model)。每个状态片段是一个独立的原子(Atom),原子之间可以组合成派生原子。
1 import { atom, useAtom } from 'jotai'; // 基础原子 const todosAtom = atom([ { id: 1, text: '学习 Jotai', completed: false }, { id: 2, text: '写一篇对比文章', completed: true }, ]); // 派生原子(计算属性) const completedTodosAtom = atom((get) => get(todosAtom).filter(t => t.completed) ); const todosCountAtom = atom((get) => get(todosAtom).length); // 写原子(可读写) const addTodoAtom = atom(null, (get, set, text) => { set(todosAtom, [ ...get(todosAtom), { id: Date.now(), text, completed: false } ]); }); function TodoCounter() { // 只会订阅 todosCountAtom 的变化 const count = useAtom(todosCountAtom); return <div>总计:{count} 项</div>; }
Jotai 的核心优势在于 精确到原子级别的重渲染控制。每个
1 | useAtom |
调用只订阅对应原子的变化,不会有多余渲染。这比 Zustand 的 selector 方案更精细,尤其适合大型复杂表单或数据可视化场景。
Jotai 还提供了丰富的工具函数:
| 功能 | API | 说明 | ||
|---|---|---|---|---|
| 异步原子 |
|
原子内直接处理异步逻辑,配合 Suspense 使用 | ||
| 存储持久化 |
|
自动序列化/反序列化到 localStorage | ||
| 原子拆分 |
|
将数组原子拆分为单个项原子,优化列表渲染 | ||
| URL 同步 |
|
原子状态自动同步到 URL hash | ||
| 不可变支持 |
|
配合 Immer 的可变语法更新不可变状态 |
Jotai 比较适合的场景包括:复杂表单(每个字段一个原子)、数据可视化面板(每个图表订阅独立的派生原子)、仪表盘应用。但在简单场景下,Jotai 的原子拆分反而增加了认知负担。
Signals:响应式编程的复兴
Signals 并不是新概念——它在 Knockout.js(2010年)和 MobX 时代就已经存在。但随着 Solid.js 的爆火和 Preact/ Angular 的跟进,Signals 在 2023-2026 年迎来了一次全面复兴。
Signals 的核心机制是 细粒度响应式(Fine-grained Reactivity):每个 Signal 是一个发布-订阅模式的值容器,读取时自动追踪依赖,写入时仅通知订阅者更新。
1 import { signal, computed, effect } from '@preact/signals'; // 创建 signal const count = signal(0); const todos = signal([ { id: 1, text: '学习 Signals', done: false } ]); // 计算属性(自动推导依赖) const completedCount = computed(() => todos.value.filter(t => t.done).length ); // 副作用(自动追踪依赖) effect(() => { console.log(`计数变化: ${count.value}`); console.log(`完成了 ${completedCount.value} 项`); }); // 更新 count.value = 5; todos.value = [...todos.value, { id: 2, text: '写代码', done: false }];
在 React 中使用 Signals:
1 import { signal } from '@preact/signals-react'; const counter = signal(0); function Counter() { // 直接使用 .value,无需 hook return ( <div> <p>计数: {counter.value}</p> <button onClick={() => counter.value++}>+1</button> </div> ); }
Signals 最大的优势是 零依赖追踪开销。与 React 的虚拟 DOM diff 不同,Signals 在值变化时直接精准更新到具体 DOM 节点。在 Solid.js 中,编译阶段就已经确定了哪些 DOM 节点依赖哪些 Signal,运行时几乎零开销。
Signals 与 React 的互操作性也是一个重要考量。@preact/signals-react 通过包装 React 的 reconciler 实现了 Signal 在 JSX 中的直接使用,但这也带来了一些边界问题,比如在 useEffect 或 useCallback 中访问 signal.value 时不会触发依赖追踪。
目前 Signals 的主流实现包括:
| 框架/库 | Signal 实现 | 特点 |
|---|---|---|
| Solid.js | 内置 createSignal | 编译时优化,性能最优 |
| Preact | @preact/signals | 与 Preact 深度集成,支持 X 平台 |
| Angular | Angular 16+ signal() | Angular 官方实现,Zone.js 替代者 |
| Vue | Vue 3 ref() | Vue 的响应式系统本质上就是 Signals |
| Svelte | runes ($state) | Svelte 5 的响应式基础 |
| 独立使用 | @preact/signals-core | 框架无关,仅 3.5KB |
四大方案深度对比
为了帮助你做出技术选型,我从以下维度对四种方案进行对比:
| 维度 | Redux (RTK) | Zustand | Jotai | Signals |
|---|---|---|---|---|
| 包体积 | ~12KB (gzip) | ~2.5KB (gzip) | ~5KB (gzip) | ~3.5KB (core) |
| 学习曲线 | 高(Action/Reducer/Store) | 低(一个 hook 搞定) | 中(原子概念需要理解) | 低(.value 直观) |
| 样板代码 | 多(即使有 RTK) | 极少 | 少 | 极少 |
| 重渲染粒度 | 组件级(通过 selector) | 组件级(通过 selector) | 原子级(精确) | DOM 级(最精细) |
| 中间件/插件生态 | 极其丰富 | 丰富 | 中等 | 较少 |
| DevTools 支持 | 顶级(Redux DevTools) | 良好(支持 Redux DevTools) | 良好 | 一般 |
| TypeScript 友好度 | 优秀 | 优秀 | 良好 | 良好 |
| 框架无关性 | React 为主 | 优秀(React/Vue/Svelte/Plain) | React 为主 | 优秀(多框架实现) |
| 适用项目规模 | 大型复杂应用 | 中小型项目 | 中大型表单/数据应用 | 重性能需求的项目 |
如何在实际项目中做选择
没有银弹——技术选型永远是场景驱动的。以下是基于实际项目经验的决策建议:
场景一:大型企业级应用(50+页面,多人团队)
推荐:Redux Toolkit + RTK Query
这类项目最看重的是 可维护性和可预测性。Redux 严格的单向数据流和约定优于配置的设计,让不同开发者写的代码风格一致。RTK Query 解决了数据请求缓存问题,大幅减少了手写 thunk/saga 的场景。使用 Redux DevTools 可以追溯到任意一个 action 的触发来源,跨团队协作时这一点至关重要。
场景二:中小型产品 / 创业项目(1-10人的团队)
推荐:Zustand
如果你在快速迭代阶段,Zustand 的简洁性会让生产力翻倍。不需要配置 store、不需要理解中间件原理、不需要 Provider 包裹——创建一个 hook 就能开干。配合 persist 中间件,状态持久化一行代码搞定。
场景三:复杂表单 / 仪表盘 / 数据可视化
推荐:Jotai
当你有大量独立状态段(比如表单的每个字段、图表面板的每个配置项),Jotai 的原子模型能完美映射这种关系。每个字段是一个独立原子,派生原子自动处理计算逻辑,splitAtom 优化列表渲染。配合 react-hook-form 使用效果更佳。
场景四:追求极致性能 / 高频更新 UI
推荐:Signals(Solid.js 或 Preact Signals)
如果你的应用有大量高频更新(实时数据流、游戏控制面板、实时协作编辑器),Signals 的细粒度更新机制是最优选择。在需要极高性能的场景下,考虑使用 Solid.js 作为 React 的替代方案。
实战:一个轻量级 TODO 应用的状态层对比
为了直观对比,我实现了一个 TODO 应用的状态层,分别用 Zustand、Jotai 和 Signals 实现同样的功能:
功能需求:
- 添加/删除/切换 TODO 项
- 计算已完成数量
- 过滤(全部/已完成/未完成)
- 本地持久化
Zustand 实现:
1 const useTodoStore = create( persist( (set, get) => ({ todos: [], filter: 'all', addTodo: (text) => set((s) => ({ todos: [...s.todos, { id: crypto.randomUUID(), text, done: false }] })), removeTodo: (id) => set((s) => ({ todos: s.todos.filter(t => t.id !== id) })), toggleTodo: (id) => set((s) => ({ todos: s.todos.map(t => t.id === id ? { ...t, done: !t.done } : t) })), setFilter: (filter) => set({ filter }), get filteredTodos() { return get().todos.filter(t => get().filter === 'all' ? true : get().filter === 'done' ? t.done : !t.done); }, }), { name: 'todo-storage' } ) );
Jotai 实现:
1 const todosAtom = atom([]); const filterAtom = atom('all'); const filteredTodosAtom = atom((get) => { const todos = get(todosAtom); const filter = get(filterAtom); return filter === 'all' ? todos : filter === 'done' ? todos.filter(t => t.done) : todos.filter(t => !t.done); });
Signals 实现:
1 const todos = signal([]); const filter = signal('all'); const filteredTodos = computed(() => { const f = filter.value; return todos.value.filter(t => f === 'all' ? true : f === 'done' ? t.done : !t.done); });
可以看到,Signals 的语法最简洁(因为 .value 直接可读写),Jotai 的原子模型最符合函数式思维,Zustand 则在简洁和功能完整之间取得了最佳平衡。
未来趋势与展望
前端状态管理正在走向 去框架化(Framework Agnostic) 和 信号驱动的细粒度响应式。几个值得关注的趋势:
- ECMAScript Signals 提案:TC39 正在推进 Signal 作为 JavaScript 语言原生的标准,这意味着未来所有框架可能共享同一套响应式基础。
- 服务端状态管理的崛起:TanStack Query / RTK Query / SWR 等数据请求库正在接管原本属于客户端状态管理的领域(缓存、同步、乐观更新)。
- 编译时优化:Solid.js 和 Svelte 展示了编译时分析状态依赖的优势,React 19 的 React Compiler 也在朝这个方向努力。
- 统一状态层:越来越多的项目不再需要单独的状态管理库——React Server Components 改变了数据获取模式,use 这个新的 React API 可以直接消费 Promise。
总结
状态管理不是一个”选最好的”问题,而是”选最适合的”问题:
- 如果你需要 大型团队协作和严格的代码规范,选择 Redux Toolkit。
- 如果你追求 开发效率和零配置的简洁性,选择 Zustand。
- 如果你的状态有 复杂的依赖关系和计算链,选择 Jotai。
- 如果你需要 极致性能和细粒度更新,选择 Signals。
更重要的是,不要被教条束缚——在一个项目中混用多种方案是完全OK的。Zustand 管理全局状态、Jotai 管理局部表单状态、Signals 管理高频更新视图,这种混合策略在大型项目中非常常见。
状态管理的本质是将 复杂度从运行时转移到架构设计阶段。选对工具,你的代码会更清晰、更可维护、更愉悦。
<
p style=”text-align:center;color:#999;font-size:14px;”>
前端状态管理是一个持续演进的话题,保持学习才能不被淘汰。
汤不热吧