Toolcoin
29.10M · 2026-03-04
同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
| 对比维度 | 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 的数据流可以记成:View → Actions → Mutations → State → View。
// 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
}
}
})
<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>
state.xxx = xxx 在严格模式下会报错,只能通过 mutation 修改。Pinia 不再区分 mutations 和 actions,只有:
// 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
}
}
})
// 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
}
})
<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() |
// 错误:直接解构会丢失响应式
const { cartCount } = useCartStore()
// 正确:用 storeToRefs
const { cartCount } = storeToRefs(useCartStore())
// 错误:没 return,组件拿不到
export const useCartStore = defineStore('cart', () => {
const count = ref(0)
function add() { count.value++ }
// 忘记 return!
})
// 正确
return { count, add }
关于这个坑的问题,我在初学的时候看到这个概念其实并不理解。所以我决定在这里展开的说一说。
在 Pinia 中,多个 store 互相调用是开发中很常见的需求(比如「订单 store」需要用到「购物车 store」的商品数据),但新手很容易因为调用时机不对写出“死锁代码”。
本节会用「大白话+实战代码」,教你安全调用的核心规则、避坑要点,以及新手最易踩的雷,保证看完就能上手。
多个 store 之间可以互相调用,但必须遵守一个黄金法则:
只存引用,延迟使用:在 store 的 setup 函数顶层,只能获取另一个 store 的「实例引用」;读取数据、调用方法的操作,必须放到函数(action)内部执行。
绝对禁止:在 setup 顶层直接读取另一个 store 的数据(会触发“互相等待”的死锁)。
以「订单 store(order.js)」调用「购物车 store(cart.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 }
})
// 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 }
})
<template>
<button @click="handleCheckout">点击下单</button>
</template>
<script setup>
// 导入订单 store
import { useOrderStore } from '@/stores/order'
const orderStore = useOrderStore()
// 点击按钮触发下单逻辑
const handleCheckout = () => {
orderStore.checkout()
}
</script>
点击按钮后,控制台会输出:
下单商品: [{ id: 1, ... }, { id: 2, ... }]
下单成功,清空购物车!
新手最疑惑的是:为什么不能在 setup 顶层直接读数据? 我们用“两个人出门”的例子,讲透背后的逻辑:
就像两个人(orderStore 和 cartStore)先各自出门(完成初始化),再互相帮忙:
组件调用 useOrderStore() → orderStore 开始初始化:只做了一件事——“记下 cartStore 的地址”(const cartStore = useCartStore()),自己先完成初始化。
后续调用 checkout() 时:orderStore 带着“地址”去找 cartStore,此时 cartStore 早就初始化好了,能顺利拿到商品数据。
如果在 setup 顶层直接读数据,就变成了两个人互相卡条件:
// 错误示例: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 的数据,只要遵守「函数内使用」的规则,完全没问题!
补全 cart.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 }
})
// 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(既不支持,也会导致初始化异常)。
// 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')
| 维度 | Vuex | Pinia |
|---|---|---|
| 概念数量 | 4 个(state/getters/mutations/actions) | 3 个(state/getters/actions) |
| 改数据方式 | 只能通过 mutation(同步) | actions 直接改(同步/异步都可) |
| 风格 | 偏“流程化” | 更接近普通函数、Composition API |
| 学习成本 | 中等 | 较低 |
一句话:Pinia 用更少的概念、更直接的方式完成同样的状态管理,而且和 Vue 3 配合更好。
一、《Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比》
二、《Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置》
跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~