2026 前端状态管理全景:从 Pinia 到 Signals 的演进之路
前言
前端状态管理是一个「永恒的话题」。从 Vuex 到 Pinia,从 Redux 到 Zustand,从 MobX 到 Signals——每隔几年就会出现新的范式和工具。到了 2026 年,状态管理领域已经形成了清晰的格局:轻量化、去中心化、细粒度响应式成为主流趋势。
本文将全面对比 Pinia、Zustand、Jotai 和 Signals 四种主流方案,从 API 设计、响应式原理到实际选型建议,帮助你在 2026 年做出最合适的选择。
一、状态管理的演进历程
1.1 从集中式到去中心化
2015-2018: 集中式 Store├── Vuex (Vue 生态)├── Redux (通用,但主要配合 React)└── 特点: 单一 Store、严格的 mutation、boilerplate 多
2019-2022: 轻量化浪潮├── Pinia (Vue 3 官方推荐)├── Zustand (极简 API)├── Jotai (原子化)└── 特点: 更少的 boilerplate、更灵活的组织方式
2023-2026: 细粒度响应式├── Signals (TC39 提案 / 各框架实现)├── Vue Vapor Mode (编译时优化)├── Solid.js Signals (先驱)└── 特点: 编译器优化、无虚拟 DOM、极致性能1.2 核心问题始终未变
无论工具怎么变,状态管理要解决的核心问题是一样的:
- 状态在哪里存储?(全局 Store / 原子 / 组件本地)
- 状态如何更新?(直接修改 / Action / 不可变更新)
- 谁需要响应变化?(订阅机制 / 细粒度依赖追踪)
- 如何避免不必要的重渲染?(选择器 / 计算属性 / Signals)
二、Pinia:Vue 生态的标准答案
2.1 核心设计
Pinia 是 Vue 3 的官方状态管理方案,它的设计哲学是「像写组合式函数一样写 Store」:
import { defineStore } from 'pinia';import { ref, computed } from 'vue';
export interface CartItem { id: string; name: string; price: number; quantity: number;}
export const useCartStore = defineStore('cart', () => { // State - 就是 ref const items = ref<CartItem[]>([]); const couponCode = ref<string | null>(null); const discount = ref(0);
// Getters - 就是 computed const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0) );
const subtotal = computed(() => items.value.reduce((sum, item) => sum + item.price * item.quantity, 0) );
const total = computed(() => { const sub = subtotal.value; return sub - sub * (discount.value / 100); });
const isEmpty = computed(() => items.value.length === 0);
// Actions - 就是普通函数 function addItem(product: Omit<CartItem, 'quantity'>) { const existing = items.value.find(item => item.id === product.id); if (existing) { existing.quantity++; } else { items.value.push({ ...product, quantity: 1 }); } }
function removeItem(id: string) { items.value = items.value.filter(item => item.id !== id); }
function updateQuantity(id: string, quantity: number) { const item = items.value.find(item => item.id === id); if (item) { if (quantity <= 0) { removeItem(id); } else { item.quantity = quantity; } } }
async function applyCoupon(code: string) { // 异步 action 直接 async/await const response = await fetch(`/api/coupons/${code}`); if (!response.ok) { throw new Error('Invalid coupon code'); } const data = await response.json(); couponCode.value = code; discount.value = data.discount; }
function clearCart() { items.value = []; couponCode.value = null; discount.value = 0; }
return { items, couponCode, discount, totalItems, subtotal, total, isEmpty, addItem, removeItem, updateQuantity, applyCoupon, clearCart, };});2.2 Pinia 的响应式原理
Pinia 的响应式核心就是 Vue 3 的 Reactivity 系统。理解这一点很重要——Pinia 并没有发明新的响应式机制,它只是提供了一种组织全局状态的方式。
// Vue 3 响应式系统的核心:Proxy// 当你写 ref([]) 时,Vue 做了什么?
// 简化版 ref 实现function ref(rawValue) { return { get value() { // 读取时收集依赖(track) track(this, 'value'); return rawValue; }, set value(newValue) { rawValue = newValue; // 写入时触发更新(trigger) trigger(this, 'value'); }, };}
// 简化版 reactive 实现(Pinia 内部用 reactive 包装 state)function reactive(target) { return new Proxy(target, { get(target, key, receiver) { const result = Reflect.get(target, key, receiver); track(target, key); // 收集依赖 // 如果结果是对象,递归代理(懒代理) if (typeof result === 'object' && result !== null) { return reactive(result); } return result; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue !== value) { trigger(target, key); // 触发更新 } return result; }, });}Pinia Store 创建时的内部流程:
// Pinia 内部(简化)function defineStore(id, setup) { return function useStore() { const pinia = inject(piniaSymbol);
if (!pinia._stores.has(id)) { // 在 effectScope 中执行 setup,确保所有响应式效果可统一清理 const scope = effectScope(true); const store = scope.run(() => { const result = setup(); // 将所有 ref 和 reactive 包装到统一的 reactive 对象中 return result; });
pinia._stores.set(id, store); }
return pinia._stores.get(id); };}2.3 Pinia 插件系统
import type { PiniaPluginContext } from 'pinia';
export function piniaPersistedState(context: PiniaPluginContext) { const { store } = context;
// 从 localStorage 恢复状态 const savedState = localStorage.getItem(`pinia-${store.$id}`); if (savedState) { store.$patch(JSON.parse(savedState)); }
// 监听状态变化并持久化 store.$subscribe((mutation, state) => { localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state)); }, { detached: true }); // detached: 组件卸载后仍保持订阅}
// main.tsimport { createPinia } from 'pinia';import { piniaPersistedState } from './plugins/persistedState';
const pinia = createPinia();pinia.use(piniaPersistedState);三、Zustand:极简主义的胜利
3.1 核心理念
Zustand 的哲学是极致简单——没有 Provider、没有 Context、没有 boilerplate。一个函数创建 Store,一个 Hook 消费状态。
import { create } from 'zustand';
interface CounterState { count: number; increment: () => void; decrement: () => void; reset: () => void; incrementBy: (amount: number) => void;}
export const useCounterStore = create<CounterState>((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }), incrementBy: (amount) => set((state) => ({ count: state.count + amount })),}));3.2 Zustand 的内部实现原理
Zustand 的源码极其精简(核心不到 100 行),其核心是发布-订阅模式:
// Zustand 核心实现(简化版)type SetState<T> = (partial: Partial<T> | ((state: T) => Partial<T>)) => void;type GetState<T> = () => T;type Listener<T> = (state: T, prevState: T) => void;
function createStore<T>(createState: (set: SetState<T>, get: GetState<T>) => T) { let state: T; const listeners = new Set<Listener<T>>();
const getState: GetState<T> = () => state;
const setState: SetState<T> = (partial) => { const prevState = state; const nextPartial = typeof partial === 'function' ? partial(state) : partial; // 浅合并(不是深合并!) const nextState = Object.assign({}, state, nextPartial);
if (!Object.is(nextState, state)) { state = nextState; // 通知所有订阅者 listeners.forEach((listener) => listener(state, prevState)); } };
const subscribe = (listener: Listener<T>) => { listeners.add(listener); return () => listeners.delete(listener); };
// 初始化状态 state = createState(setState, getState);
return { getState, setState, subscribe };}注意一个关键点:Zustand 使用浅合并(shallow merge),而不是像 Pinia 那样的 Proxy 深度响应式。这意味着嵌套对象的更新需要显式创建新引用:
// ❌ 这不会触发更新(直接修改嵌套对象)set((state) => { state.user.name = 'Alice'; // 浅合并检测不到嵌套变化 return state;});
// ✅ 正确方式:创建新引用set((state) => ({ user: { ...state.user, name: 'Alice' },}));3.3 中间件模式
Zustand 的中间件采用高阶函数组合:
import { create } from 'zustand';import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';import { immer } from 'zustand/middleware/immer';
interface TodoState { todos: Array<{ id: number; text: string; done: boolean }>; addTodo: (text: string) => void; toggleTodo: (id: number) => void;}
export const useTodoStore = create<TodoState>()( devtools( persist( immer( subscribeWithSelector((set) => ({ todos: [], addTodo: (text) => set((state) => { // immer 中间件允许直接修改(内部产生不可变更新) state.todos.push({ id: Date.now(), text, done: false, }); }), toggleTodo: (id) => set((state) => { const todo = state.todos.find((t) => t.id === id); if (todo) todo.done = !todo.done; }), })) ), { name: 'todo-storage' } // persist 配置 ), { name: 'TodoStore' } // devtools 配置 ));
// 精确订阅:只在 todos 长度变化时触发useTodoStore.subscribe( (state) => state.todos.length, (length, prevLength) => { console.log(`Todo count changed: ${prevLength} → ${length}`); });四、Jotai:原子化状态管理
4.1 原子化思想
Jotai 的灵感来自 Recoil,但更加简洁。它的核心概念是原子(Atom)——每个状态都是一个独立的原子,原子之间可以组合派生。
import { atom } from 'jotai';
// 基础原子const countAtom = atom(0);const nameAtom = atom('World');
// 派生原子(只读)const greetingAtom = atom((get) => { const name = get(nameAtom); const count = get(countAtom); return `Hello, ${name}! You clicked ${count} times.`;});
// 可写派生原子const incrementAtom = atom( null, // 读取值为 null(只写原子) (get, set) => { set(countAtom, get(countAtom) + 1); });
// 异步原子const userAtom = atom(async () => { const response = await fetch('/api/user'); return response.json();});
// 带缓存的异步原子const userByIdAtom = atom(async (get) => { const id = get(userIdAtom); const response = await fetch(`/api/users/${id}`); return response.json();});4.2 Jotai 的依赖追踪原理
Jotai 的核心是一个基于图的依赖追踪系统:
// Jotai 内部原理(简化)class Store { // 每个原子的当前值 private atomValues = new WeakMap<Atom, unknown>(); // 依赖图:原子 A 依赖了哪些原子 private atomDependencies = new WeakMap<Atom, Set<Atom>>(); // 反向依赖:谁依赖了原子 A private atomDependents = new WeakMap<Atom, Set<Atom>>(); // 订阅者(UI 组件) private atomSubscribers = new WeakMap<Atom, Set<() => void>>();
get<T>(atom: Atom<T>): T { if (typeof atom.read === 'function') { // 派生原子:执行 read 函数并追踪依赖 const dependencies = new Set<Atom>(); const getter = (depAtom: Atom) => { dependencies.add(depAtom); // 注册反向依赖 this.addDependent(depAtom, atom); return this.get(depAtom); };
const value = atom.read(getter); this.atomDependencies.set(atom, dependencies); this.atomValues.set(atom, value); return value as T; }
// 基础原子:直接返回值 return (this.atomValues.get(atom) ?? atom.init) as T; }
set<T>(atom: Atom<T>, value: T): void { const prevValue = this.atomValues.get(atom); if (Object.is(prevValue, value)) return;
this.atomValues.set(atom, value);
// 通知所有依赖此原子的派生原子重新计算 this.notifyDependents(atom);
// 通知 UI 订阅者 this.notifySubscribers(atom); }
private notifyDependents(atom: Atom): void { const dependents = this.atomDependents.get(atom); if (!dependents) return;
for (const dependent of dependents) { // 重新计算派生原子 this.get(dependent); // 递归通知 this.notifyDependents(dependent); this.notifySubscribers(dependent); } }}4.3 实用模式
import { atom } from 'jotai';import { atomWithStorage, atomWithDefault, splitAtom } from 'jotai/utils';
// 持久化原子 - 自动同步到 localStorageconst themeAtom = atomWithStorage('theme', 'light');
// 带默认值的异步原子const configAtom = atomWithDefault(async () => { const res = await fetch('/api/config'); return res.json();});
// 列表拆分 - 每个列表项成为独立原子interface Todo { id: number; text: string; done: boolean;}
const todosAtom = atom<Todo[]>([]);const todoAtomsAtom = splitAtom(todosAtom);// todoAtomsAtom 的值是 Atom<Todo>[] —— 每个 todo 是独立原子// 修改单个 todo 不会导致整个列表重新渲染
// 族原子 - 参数化创建const todoAtomFamily = (id: number) => atom( (get) => get(todosAtom).find((t) => t.id === id), (get, set, update: Partial<Todo>) => { set(todosAtom, (prev) => prev.map((t) => (t.id === id ? { ...t, ...update } : t)) ); } );五、Signals:响应式的未来
5.1 什么是 Signals?
Signals 是一种细粒度响应式原语,最早由 Solid.js 推广,现在已成为 TC39 Stage 1 提案,多个框架都提供了自己的实现。
Signals 的核心理念:值的变化应该自动、精确地传播到使用它的地方,无需虚拟 DOM diff。
// TC39 Signal 提案的 API(2026 年规范草案)// 注意:实际语法可能随提案演进而变化
// 基础 Signalconst count = new Signal.State(0);console.log(count.get()); // 0count.set(1);console.log(count.get()); // 1
// 计算 Signal(自动追踪依赖)const doubled = new Signal.Computed(() => count.get() * 2);console.log(doubled.get()); // 2
// 当 count 变化时,doubled 自动更新count.set(5);console.log(doubled.get()); // 105.2 Signals 的响应式原理
Signals 的核心是自动依赖追踪 + 惰性求值 + 推拉混合更新:
// Signals 内部原理(简化实现)let currentComputed: Computed | null = null;
class Signal<T> { private value: T; private subscribers = new Set<Computed>(); private version = 0;
constructor(initialValue: T) { this.value = initialValue; }
get(): T { // 如果当前有 Computed 正在执行,自动注册依赖 if (currentComputed) { this.subscribers.add(currentComputed); currentComputed.dependencies.add(this); } return this.value; }
set(newValue: T): void { if (Object.is(this.value, newValue)) return; this.value = newValue; this.version++;
// 通知所有依赖的 Computed:你的依赖变了,标记为脏 for (const sub of this.subscribers) { sub.markDirty(); } }}
class Computed<T> { private value: T | undefined; private dirty = true; private fn: () => T; dependencies = new Set<Signal>(); private subscribers = new Set<Computed>();
constructor(fn: () => T) { this.fn = fn; }
get(): T { // 自动依赖追踪 if (currentComputed) { this.subscribers.add(currentComputed); }
// 惰性求值:只在被读取且标记为脏时才重新计算 if (this.dirty) { this.recompute(); } return this.value!; }
markDirty(): void { if (!this.dirty) { this.dirty = true; // 级联标记下游为脏 for (const sub of this.subscribers) { sub.markDirty(); } } }
private recompute(): void { // 清除旧依赖 for (const dep of this.dependencies) { dep.subscribers?.delete(this); } this.dependencies.clear();
// 设置当前追踪上下文 const prev = currentComputed; currentComputed = this;
try { this.value = this.fn(); } finally { currentComputed = prev; }
this.dirty = false; }}
// 效果(副作用)class Effect { private fn: () => void; private dirty = true; dependencies = new Set<Signal>();
constructor(fn: () => void) { this.fn = fn; this.run(); // 立即执行一次以收集依赖 }
run(): void { const prev = currentComputed; currentComputed = this as any; try { this.fn(); } finally { currentComputed = prev; } this.dirty = false; }
markDirty(): void { if (!this.dirty) { this.dirty = true; // 调度异步执行(微任务) queueMicrotask(() => { if (this.dirty) this.run(); }); } }}5.3 Vue 的 Signals 演进:Vapor Mode
Vue 的 Vapor Mode 是 Signals 理念在 Vue 生态中的体现——编译时优化,绕过虚拟 DOM,直接操作 DOM:
<!-- 传统 Vue 组件 --><script setup>import { ref, computed } from 'vue';
const count = ref(0);const doubled = computed(() => count.value * 2);</script>
<template> <div> <p>Count: {{ count }}</p> <p>Doubled: {{ doubled }}</p> <button @click="count++">+1</button> </div></template>// Vapor Mode 编译后(概念性示例)// 不再生成虚拟 DOM,直接生成 DOM 操作指令import { ref, computed, renderEffect, setText, on } from 'vue/vapor';
export default () => { const count = ref(0); const doubled = computed(() => count.value * 2);
// 直接创建 DOM 节点 const div = document.createElement('div'); const p1 = document.createElement('p'); const p2 = document.createElement('p'); const button = document.createElement('button');
// 细粒度绑定:只更新变化的文本节点 renderEffect(() => setText(p1, `Count: ${count.value}`)); renderEffect(() => setText(p2, `Doubled: ${doubled.value}`));
button.textContent = '+1'; on(button, 'click', () => count.value++);
div.append(p1, p2, button); return div;};5.4 Solid.js 的 Signals(成熟实现参考)
虽然本文不聚焦 Solid.js 框架本身,但它的 Signals 实现是目前最成熟的参考:
// Solid.js 风格的 Signals(框架无关的理解)import { createSignal, createMemo, createEffect, batch } from 'solid-js';
// 基础信号const [count, setCount] = createSignal(0);const [name, setName] = createSignal('World');
// 派生计算(自动追踪依赖)const greeting = createMemo(() => `Hello ${name()}, count is ${count()}`);
// 副作用(自动追踪并在依赖变化时重新执行)createEffect(() => { console.log(greeting()); // 当 count 或 name 变化时自动执行});
// 批量更新(避免中间状态触发多次计算)batch(() => { setCount(10); setName('Alice'); // effect 只会执行一次});六、四大方案对比
6.1 API 设计对比
// === Pinia ===// 定义const useStore = defineStore('id', () => { const count = ref(0); const doubled = computed(() => count.value * 2); const increment = () => count.value++; return { count, doubled, increment };});// 使用(Vue 组件中)const store = useStore();store.increment();console.log(store.count, store.doubled);
// === Zustand ===// 定义const useStore = create((set, get) => ({ count: 0, doubled: () => get().count * 2, // 注意:不是自动计算的 increment: () => set((s) => ({ count: s.count + 1 })),}));// 使用const count = useStore((s) => s.count); // 选择器
// === Jotai ===// 定义const countAtom = atom(0);const doubledAtom = atom((get) => get(countAtom) * 2);const incrementAtom = atom(null, (get, set) => { set(countAtom, get(countAtom) + 1);});// 使用const [count, setCount] = useAtom(countAtom);const doubled = useAtomValue(doubledAtom);
// === Signals (TC39) ===// 定义const count = new Signal.State(0);const doubled = new Signal.Computed(() => count.get() * 2);// 使用count.set(count.get() + 1);console.log(doubled.get());6.2 性能特性对比
特性 | Pinia | Zustand | Jotai | Signals-------------|------------|------------|------------|------------响应式粒度 | 属性级 | Store 级 | 原子级 | 值级依赖追踪 | 自动(Proxy)| 手动(选择器)| 自动 | 自动更新传播 | Push | Pull | Push+Pull | Push+Pull(惰性)虚拟DOM | 需要 | 需要 | 需要 | 不需要SSR支持 | 优秀 | 良好 | 良好 | 框架依赖DevTools | Vue DevTools| Redux DT | Jotai DT | 发展中TypeScript | 优秀 | 优秀 | 优秀 | 优秀包体积 | ~2KB | ~1KB | ~3KB | 0(原生)6.3 更新性能的本质差异
// 假设有 1000 个组件订阅了同一个 Store// Store 中有 100 个属性,但只更新了 1 个属性
// Pinia (Vue):// - Proxy 精确追踪到哪些组件用了哪个属性// - 只有用到被修改属性的组件会重新渲染// - 但仍需经过虚拟 DOM diff
// Zustand:// - 依赖开发者写选择器来避免不必要的重渲染// - 没写选择器 → 所有消费者都会被通知// - 有 shallow 比较优化
// Jotai:// - 每个原子独立,更新一个原子只影响订阅了它的组件// - 天然细粒度,不需要手动优化
// Signals:// - 最细粒度:更新直接传播到使用该值的 DOM 节点// - 跳过虚拟 DOM diff// - 理论上是性能最优的方案七、选型建议
7.1 决策树
你用 Vue 吗?├── 是 → Pinia(官方方案,生态最好)│ └── 需要极致性能?→ 关注 Vue Vapor Mode└── 否 → 你的状态结构是怎样的? ├── 集中式大 Store → Zustand(简单直接) ├── 分散的独立状态 → Jotai(原子化更自然) └── 追求极致性能 → Signals(Solid.js 等)7.2 具体场景建议
// 场景 1: Vue 3 中大型项目// 推荐: Pinia// 理由: 官方支持、DevTools 完美集成、团队学习成本低const useAuthStore = defineStore('auth', () => { /* ... */ });const useCartStore = defineStore('cart', () => { /* ... */ });
// 场景 2: 需要跨框架共享状态// 推荐: Zustand 或 Signals// 理由: 框架无关,可以在任何环境中使用const store = createStore((set) => ({ theme: 'light', toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),}));
// 场景 3: 表单状态管理(大量独立字段)// 推荐: Jotai// 理由: 每个字段是独立原子,修改一个字段不会触发其他字段重渲染const nameAtom = atom('');const emailAtom = atom('');const ageAtom = atom(0);const formValidAtom = atom((get) => { return get(nameAtom).length > 0 && get(emailAtom).includes('@');});
// 场景 4: 高频更新的实时数据(如股票、游戏)// 推荐: Signals// 理由: 最小化更新开销,跳过虚拟 DOMconst price = new Signal.State(100.0);const change = new Signal.Computed(() => { const p = price.get(); return ((p - 100) / 100 * 100).toFixed(2) + '%';});八、未来趋势
8.1 编译器驱动的优化
2026 年的明确趋势是:运行时响应式正在向编译时优化迁移。
- Vue Vapor Mode 在编译时分析模板,生成直接的 DOM 操作代码
- Svelte 5 的 Runes 将响应式变成编译时特性
- Signals TC39 提案一旦进入规范,所有框架都可以基于原生实现优化
8.2 状态管理的「消失」
最好的状态管理是你感觉不到它的存在。随着框架和语言层面对细粒度响应式的支持越来越好,独立的状态管理库可能会变得越来越薄——它们会聚焦在组织结构和开发体验上,而不是响应式机制本身。
总结
| 方案 | 适合场景 | 核心优势 | 主要局限 |
|---|---|---|---|
| Pinia | Vue 生态 | 官方支持、DX 好 | 绑定 Vue |
| Zustand | 需要简单方案 | 极简、灵活 | 需手动优化选择器 |
| Jotai | 分散独立状态 | 原子化、自动优化 | 心智模型较新 |
| Signals | 追求极致性能 | 最细粒度、无 VDOM | 生态尚在发展 |
没有银弹。选择状态管理方案的关键不是「哪个最好」,而是「哪个最适合你的项目、团队和场景」。理解每种方案的原理和取舍,才能做出真正有依据的决策。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!