
这三个库不是凭空冒出来的。它们要解决的,是同一个根本问题:
当很多组件都要读写同一份"全局状态"时,怎么既不让代码乱成一团,又保证状态的变化可追踪、可预测?
想象一间开放式办公室里有一块公共白板。谁都可以看,谁都可以改。如果不立规矩,三个人同时改、你不知道最后那行字是谁写的、为什么改——bug 就来了。
用 props 和事件也能传状态,但当组件树很深、要共享状态的组件离得很远时,一层层往下传(prop drilling)会让人崩溃。于是大家把状态提到一个"全局仓库"里。
但全局可变状态本身是个危险源。这三个库,本质上是三套不同的"白板使用守则"。
Redux:规矩最严的那套守则
核心三条:
- 单一数据源:整个应用的状态是一棵树,装在一个 store 里。
- 状态只读:不能直接改。想改,就发一个 action(一个描述"发生了什么"的普通对象)。
-
用纯函数改状态:reducer 接收
(旧状态, action),返回新状态。它不修改入参,也不搞副作用。
import { createStore } from 'redux'
// reducer:纯函数,接收 (旧state, action) 返回新 state
function counter(state = { value: 0 }, action) {
switch (action.type) {
case 'incremented':
return { ...state, value: state.value + 1 } // 返回新对象,不改动旧的
default:
return state
}
}
// store 由 createStore 创建:装着整棵 state 树,暴露 dispatch / getState / subscribe
const store = createStore(counter)
store.getState() // { value: 0 }
store.dispatch({ type: 'incremented' })
store.getState() // { value: 1 }
异步怎么办?reducer 必须是纯函数,不能塞异步。所以异步逻辑放进 thunk / saga 这类 middleware,等数据回来了再 dispatch 一个普通 action。
关键点:Redux 内核本身跟框架无关。它能在 React、Vue、Angular、甚至原生 JS 里用。真正把它接进 React 的是 react-redux。
现代写法:官方现在推荐 Redux Toolkit (RTK)。它用 Immer 库,让你写出"像是直接改"的代码,底层其实还是生成不可变的新状态:
const counter = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
incremented: (state) => { state.value += 1 } // 看着像改,实际是 Immer 生成新状态
}
})
// createSlice 只产出 reducer 和 action,还没造 store。
// 真正创建 store 用的是 configureStore(内部仍是 createStore,但默认挂好 thunk、DevTools、reducer 合并)
const store = configureStore({ reducer: counter })
这套严格规矩换来了什么?状态变化完全可预测、易测试,而且天然支持"时间旅行调试"——因为每一步都是"旧状态 + action → 新状态",DevTools 能把整条历史重放。
Vuex:给 Vue 量身定做的车间
Vuex 同样管全局状态,但它长在 Vue 的响应式系统上,概念比 Redux 多一层:
- state:状态
- getters:基于 state 计算出来的派生值(类似 computed)
- mutations:唯一能改 state 的地方,且必须是同步的
-
actions:可以异步,但不能直接改 state,只能
commit一个 mutation
const store = createStore({
state: () => ({ count: 0 }),
mutations: {
INCREMENT(state) { state.count++ } // 同步、唯一的改法
},
actions: {
increment({ commit }) { commit('INCREMENT') } // 异步逻辑在这里,改状态还得 commit
}
})
为什么要把"改状态"拆成 mutation 和 action 两步?因为 mutation 强制同步,DevTools 就能把每一次状态变更都记录成一个清晰的快照,支持时间旅行。这是 Vuex 当初的设计取舍:用一点样板代码,换调试可见性。
Pinia:把车间里多余的脚手架拆了
Pinia 一开始就是按"Vuex 5 该长什么样"探索出来的,现在它成了 Vue 的官方推荐,Vuex 进入维护模式(只修 bug,不加新功能)。
它做的最关键的一刀:砍掉 mutations。
export const useCounter = defineStore('counter', {
state: () => ({ count: 0 }),
getters: { double: (s) => s.count * 2 },
actions: {
increment() { this.count++ }, // 同步直接改
async load() { this.count = await api() } // 异步也能直接改
}
})
变化在哪:
- 概念从四个变三个:state、getters、actions。action 把 Vuex 里 mutation 和 action 的活儿全干了。
- 直接改状态也会被追踪:Pinia 跑在 Vue 响应式系统上,状态的每一次变动都被记录下来,DevTools 照样能时间旅行——所以"去掉 mutation 会丢失可追溯性"这个担心不成立。真正去掉的,只是"强制同步修改"那条约束,而那条约束主要服务于 DevTools,不是服务于你的业务逻辑。
-
多个扁平 store,不再是嵌套 module:每个 store 独立、可摇树优化,不用再写
namespaced: true那套命名空间仪式。 - TypeScript 一等公民:类型从 store 定义自动推断,不用像 Vuex 那样手动做类型增强。
- 两种写法:Options(像 Vuex,好迁移)和 Setup(直接用语法的 ref / computed,最灵活)。
一张表看清差别
| 维度 | Redux (RTK) | Vuex 4 | Pinia |
|---|---|---|---|
| 适用框架 | 框架无关(常配 React) | Vue | Vue |
| 改状态的入口 | dispatch action → reducer | commit mutation(同步唯一入口) | action 里直接改 / $patch
|
| 异步放哪 | middleware (thunk/saga) | actions | actions |
| 状态是否可变 | 不可变(RTK 用 Immer 伪装可改) | 可变(mutation 里直接改) | 可变(action 里直接改) |
| store 结构 | 单一 store + slice | 单一 store + 嵌套 module | 多个扁平 store |
| TypeScript | 需配置,RTK 体验好 | 手动类型增强 | 开箱推断 |
| 时间旅行调试 | 有 | 有 | 有 |
| 当前地位 | React 生态主流 | 维护模式 | Vue 官方推荐 |
怎么选?
- React 项目 / 要框架无关:Redux 仍是稳妥主流。它规矩多、样板多,但换来最强的可预测性和最庞大的生态(RTK Query、丰富 middleware、成熟的 DevTools)。新项目直接用 Redux Toolkit,别再手写 switch/action-type 那套老写法。
- 新 Vue 3 项目:默认 Pinia。官方推荐、代码更少、TS 体验最好。
- 老 Vue 2/3 项目已经在用 Vuex:不用急着迁。Vuex 还能用;只有当你本来就在做 Vue 3 升级时,才值得顺手迁到 Pinia。两者可并存,逐步迁移。
一个常见的误解值得点破:"直接改状态"不等于"失控"。Redux 的不可变更新,价值在于用引用相等做廉价的变化检测、以及支持时间旅行;Vuex 的 mutation,价值在于强制同步以便 DevTools 记录。Pinia 借 Vue 的响应式系统,在不强制不可变、不强制 mutation 的前提下,照样拿到了可追溯性。所以选哪个,不是"谁更严谨"的问题,而是"你的框架和团队更需要哪套规矩"的问题。