欢迎光临

前端状态管理演进:从 Redux 到 Zustand、Jotai 与 Signals 的深度对比与实践

引言:前端状态管理的三十年战争

前端状态管理是一个永不过时的话题。从早期 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 说明
异步原子
1
atom(async (get) => ...)
原子内直接处理异步逻辑,配合 Suspense 使用
存储持久化
1
createJSONStorage
自动序列化/反序列化到 localStorage
原子拆分
1
splitAtom
将数组原子拆分为单个项原子,优化列表渲染
URL 同步
1
atomWithHash
原子状态自动同步到 URL hash
不可变支持
1
atomWithImmer
配合 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;”>前端状态管理概念图前端状态管理是一个持续演进的话题,保持学习才能不被淘汰。

【本站文章皆为原创,未经允许不得转载】:汤不热吧 » 前端状态管理演进:从 Redux 到 Zustand、Jotai 与 Signals 的深度对比与实践
分享到: 更多 (0)