同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。
一、先搞清楚:Vuex 和 Pinia 到底是啥?
1.1 一句话认识它们
- Vuex:Vue 2 时代的官方状态管理库,通过集中式存储管理应用的全部状态。
- Pinia:Vue 3 时代的推荐状态管理库,作者和 Vue 核心团队同一人,被当作 Vuex 5 的正式版。
1.2 为什么大家纷纷从 Vuex 迁到 Pinia?
| 对比维度 | Vuex | Pinia |
|---|---|---|
| 心智负担 | 概念多(state/mutations/actions/getters) | 概念简单(一个 store,其余都是普通函数) |
| TypeScript | 类型支持一般 | 原生支持好 |
| 模块化 | 需自己设计 modules | 天然多 store,无嵌套 |
| Composition API | 需配合 useStore 等 |
天然适配 setup |
| 打包体积 | 相对大 | 更小 |
| 官方推荐 | Vue 2 主力,Vue 3 仍可用 | Vue 3 推荐首选 |
一句话:Pinia 更简单、更贴近 Vue 3,写起来更像普通 JS。
二、Vuex 核心用法:四大金刚
2.1 整体结构回顾
Vuex 的数据流可以记成:View → Actions → Mutations → State → View。
- state:唯一数据源
- getters:可理解为"计算属性"
- mutations:唯一能改 state 的地方(必须同步)
- actions:可以异步,内部再 commit mutations
2.2 完整示例:用户购物车
js
// store/index.js (Vuex)
import { createStore } from 'vuex'
export default createStore({
state: {
cartItems: [], // 购物车商品
user: null // 当前用户
},
getters: {
// 购物车商品数量
cartCount(state) {
return state.cartItems.reduce((sum, item) => sum + item.quantity, 0)
},
// 购物车总价
cartTotal(state) {
return state.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
mutations: {
addToCart(state, product) {
const exist = state.cartItems.find(item => item.id === product.id)
if (exist) {
exist.quantity++
} else {
state.cartItems.push({ ...product, quantity: 1 })
}
},
setUser(state, user) {
state.user = user
}
},
actions: {
// 异步:模拟登录
async login({ commit }, credentials) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const user = await res.json()
commit('setUser', user)
return user
}
}
})
2.3 在组件里怎么用?
html
<template>
<div>
<p>购物车:{{ cartCount }} 件,总价:{{ cartTotal }}</p>
<button @click="addProduct">加入购物车</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// 读 state / getters
const cartCount = computed(() => store.getters.cartCount)
const cartTotal = computed(() => store.getters.cartTotal)
// 触发 mutation(同步)
function addProduct() {
store.commit('addToCart', { id: 1, name: '商品A', price: 99 })
}
// 触发 action(异步)
async function handleLogin() {
await store.dispatch('login', { username: 'admin', password: '123' })
}
</script>
2.4 Vuex 容易踩的坑
- 忘记 mutations :直接
state.xxx = xxx在严格模式下会报错,只能通过 mutation 修改。 - 在 mutation 里写异步:理论上必须同步,写异步会导致难以追踪、调试困难。
- 命名冲突:多个 module 时,getter/mutation/action 可能重名,需要命名空间。
三、Pinia 核心用法:一个 Store 搞定
3.1 设计思路
Pinia 不再区分 mutations 和 actions,只有:
- state:数据
- getters:计算属性
- actions:既可以同步也可以异步,直接改 state
3.2 完整示例:同一需求用 Pinia 写
js
// stores/cart.js (Pinia - Options 风格)
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
cartItems: [],
user: null
}),
getters: {
cartCount(state) {
return state.cartItems.reduce((sum, item) => sum + item.quantity, 0)
},
cartTotal(state) {
return state.cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
actions: {
addToCart(product) {
const exist = this.cartItems.find(item => item.id === product.id)
if (exist) {
exist.quantity++
} else {
this.cartItems.push({ ...product, quantity: 1 })
}
},
async login(credentials) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const user = await res.json()
this.user = user // 直接改 state,不需要 mutation!
return user
}
}
})
3.3 Setup Store 风格(更贴近 Composition API)
js
// stores/cart.js (Pinia - Setup Store 风格)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// state:用 ref/reactive
const cartItems = ref([])
const user = ref(null)
// getters:用 computed
const cartCount = computed(() =>
cartItems.value.reduce((sum, item) => sum + item.quantity, 0)
)
const cartTotal = computed(() =>
cartItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// actions:普通函数
function addToCart(product) {
const exist = cartItems.value.find(item => item.id === product.id)
if (exist) {
exist.quantity++
} else {
cartItems.value.push({ ...product, quantity: 1 })
}
}
async function login(credentials) {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials)
})
const data = await res.json()
user.value = data
return data
}
// 必须 return 出去,组件才能用
return {
cartItems,
user,
cartCount,
cartTotal,
addToCart,
login
}
})
3.4 在组件里怎么用?
html
<template>
<div>
<p>购物车:{{ cartStore.cartCount }} 件,总价:{{ cartStore.cartTotal }}</p>
<button @click="cartStore.addToCart({ id: 1, name: '商品A', price: 99 })">
加入购物车
</button>
</div>
</template>
<script setup>
import { useCartStore } from '@/stores/cart'
const cartStore = useCartStore()
// 用 storeToRefs 解构,保持响应式(getters 和 state 需要)
// 如果直接解构 const { cartCount } = cartStore,会丢失响应式!
import { storeToRefs } from 'pinia'
const { cartCount, cartTotal } = storeToRefs(cartStore)
// actions 直接解构没问题
const { addToCart, login } = cartStore
</script>
四、核心概念对照表
| 概念 | Vuex | Pinia (Options) | Pinia (Setup) |
|---|---|---|---|
| 定义数据 | state: { } |
state: () => ({ }) |
ref() / reactive() |
| 计算属性 | getters: { } |
getters: { } |
computed() |
| 修改数据(同步) | mutations |
在 actions 里直接改 this.xxx |
直接改 ref.value |
| 修改数据(异步) | actions + commit |
actions 里直接改 |
在函数里直接改 |
| 组件调用 | store.commit() / store.dispatch() |
store.xxx() |
store.xxx() |
五、迁移时的常见坑
5.1 解构 store 丢响应式
javascript
// ❌ 错误:直接解构会丢失响应式
const { cartCount } = useCartStore()
// ✅ 正确:用 storeToRefs
const { cartCount } = storeToRefs(useCartStore())
5.2 Setup Store 忘记 return
javascript
// ❌ 错误:没 return,组件拿不到
export const useCartStore = defineStore('cart', () => {
const count = ref(0)
function add() { count.value++ }
// 忘记 return!
})
// ✅ 正确
return { count, add }
5.3 多个 store 之间互相调用
关于这个坑的问题,我在初学的时候看到这个概念其实并不理解。所以我决定在这里展开的说一说。
在 Pinia 中,多个 store 互相调用是开发中很常见的需求(比如「订单 store」需要用到「购物车 store」的商品数据),但新手很容易因为调用时机不对写出"死锁代码"。
本节会用「大白话+实战代码」,教你安全调用的核心规则 、避坑要点 ,以及新手最易踩的雷,保证看完就能上手。
一、核心结论(先记重点)
多个 store 之间可以互相调用,但必须遵守一个黄金法则:
✅ 只存引用,延迟使用 :在 store 的 setup 函数顶层,只能获取另一个 store 的「实例引用」;读取数据、调用方法 的操作,必须放到函数(action)内部执行。
❌ 绝对禁止:在 setup 顶层直接读取另一个 store 的数据(会触发"互相等待"的死锁)。
二、安全写法(直接抄作业)
以「订单 store(order.js)」调用「购物车 store(cart.js)」为例,实现下单时获取购物车商品的功能。
步骤1:创建被调用的购物车 store(cart.js)
js
// stores/cart.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore = defineStore('cart', () => {
// 购物车商品列表(state)
const cartItems = ref([
{ id: 1, name: '新手小白入门教程', price: 99 },
{ id: 2, name: 'Pinia 避坑手册', price: 59 }
])
// 简单的方法(action),方便后续被调用
function clearCart() {
cartItems.value = []
}
return { cartItems, clearCart }
})
步骤2:创建调用方订单 store(order.js)
js
// stores/order.js
import { defineStore } from 'pinia'
// 1. 导入购物车 store 的创建函数
import { useCartStore } from './cart'
export const useOrderStore = defineStore('order', () => {
// ✅ 安全操作:在 setup 顶层只获取 cartStore 的「实例引用」
// 此时只是"记下来购物车的地址",不会读取数据、不会触发死锁
const cartStore = useCartStore()
// 2. 核心:把"使用 cartStore"的逻辑,放到 action 函数内部
function checkout() {
// 🎯 延迟使用:只有调用 checkout 时,才会真正读取 cartStore 的数据
// 此时两个 store 都已初始化完成,数据可正常获取
console.log('下单商品:', cartStore.cartItems)
// 也可以调用另一个 store 的方法
if (cartStore.cartItems.length > 0) {
console.log('下单成功,清空购物车!')
cartStore.clearCart()
} else {
console.log('购物车为空,无法下单!')
}
}
return { checkout }
})
步骤3:在组件中使用(验证效果)
html
<template>
<button @click="handleCheckout">点击下单</button>
</template>
<script setup>
// 导入订单 store
import { useOrderStore } from '@/stores/order'
const orderStore = useOrderStore()
// 点击按钮触发下单逻辑
const handleCheckout = () => {
orderStore.checkout()
}
</script>
点击按钮后,控制台会输出:
Plain
下单商品: [{ id: 1, ... }, { id: 2, ... }]
下单成功,清空购物车!
三、为什么要这样写?(大白话讲透"死锁")
新手最疑惑的是:为什么不能在 setup 顶层直接读数据? 我们用"两个人出门"的例子,讲透背后的逻辑:
1. 安全写法的执行流程(无死锁)
就像两个人(orderStore 和 cartStore)先各自出门(完成初始化),再互相帮忙:
-
组件调用
useOrderStore()→ orderStore 开始初始化:只做了一件事------"记下 cartStore 的地址"(const cartStore = useCartStore()),自己先完成初始化。 -
后续调用
checkout()时:orderStore 带着"地址"去找 cartStore,此时 cartStore 早就初始化好了,能顺利拿到商品数据。
2. 新手踩坑写法(触发死锁)
如果在 setup 顶层直接读数据,就变成了两个人互相卡条件:
js
// ❌ 错误示例:order.js(千万别这么写!)
export const useOrderStore = defineStore('order', () => {
const cartStore = useCartStore()
// ❌ 致命错误:setup 顶层直接读取 cartStore 的数据
const goods = cartStore.cartItems
function checkout() {
console.log(goods)
}
return { checkout }
})
此时的执行流程就会"僵持住":
-
orderStore 初始化时,要求"先拿到 cartStore 的商品数据,才能完成初始化"。
-
于是去调用
useCartStore(),让 cartStore 初始化。 -
如果 cartStore 也在顶层读 orderStore 的数据,就会变成:order 等 cart 给数据,cart 等 order 给数据,俩人都卡着不动 → 代码报错(死锁)。
四、进阶:两个 store 互相调用(依然安全)
如果购物车 store 也需要调用订单 store 的数据,只要遵守「函数内使用」的规则,完全没问题!
补全 cart.js,新增"查看订单状态"的方法:
js
// stores/cart.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 导入订单 store
import { useOrderStore } from './order'
export const useCartStore = defineStore('cart', () => {
const cartItems = ref([{ id: 1, name: '新手小白入门教程', price: 99 }])
// ✅ 安全:在函数内调用订单 store
function checkOrderStatus() {
const orderStore = useOrderStore()
// 假设 orderStore 有一个 orderStatus 状态
console.log('当前订单状态:', orderStore.orderStatus)
}
function clearCart() {
cartItems.value = []
}
return { cartItems, clearCart, checkOrderStatus }
})
js
// stores/order.js 补充 orderStatus 状态
export const useOrderStore = defineStore('order', () => {
const cartStore = useCartStore()
// 新增订单状态
const orderStatus = ref('未支付')
function checkout() {
console.log('下单商品:', cartStore.cartItems)
orderStatus.value = '已支付'
}
return { checkout, orderStatus }
})
此时两个 store 互相调用,但因为所有"使用对方"的逻辑都在函数内,初始化阶段互不干扰,完全不会死锁!
五、汇总一下
-
✅
defineStore是同步函数 ,其 setup 回调不能加 async(异步逻辑只能写在 action 里)。 -
✅ 跨 store 调用的核心:setup 顶层只存引用,函数内部才使用。
-
❌ 禁止在 setup 顶层直接读取另一个 store 的 state/getters(必触发死锁)。
-
❌ 禁止在 setup 顶层使用
await(既不支持,也会导致初始化异常)。
5.4 Pinia 需要先挂载
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // 必须在 createApp 之后、mount 之前
app.mount('#app')
六、日常开发该怎么选?
- 新项目(Vue 3):优先用 Pinia,尤其 Setup Store 风格,和 Composition API 很契合。
- 老项目(Vue 2 + Vuex):如果项目稳定、迁移成本高,可以先不急着迁;要升级 Vue 3 时,顺带迁到 Pinia 更合适。
- 团队习惯:如果团队已经统一用 Vuex 且运转良好,不必为了"新"而强行迁移,关键是统一和维护成本。
七、总结
| 维度 | Vuex | Pinia |
|---|---|---|
| 概念数量 | 4 个(state/getters/mutations/actions) | 3 个(state/getters/actions) |
| 改数据方式 | 只能通过 mutation(同步) | actions 直接改(同步/异步都可) |
| 风格 | 偏"流程化" | 更接近普通函数、Composition API |
| 学习成本 | 中等 | 较低 |
一句话:Pinia 用更少的概念、更直接的方式完成同样的状态管理,而且和 Vue 3 配合更好。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~