或者yarn add pinia
不少刚接触Vue3的同学,一提到「Store」就犯懵——这东西到底是干啥的?和之前Vue2里的Vuex有啥区别?现在项目里该选Pinia还是Vuex?别慌,这篇文章把Vue3 Store从基础概念到实战细节,再到避坑技巧全拆明白,不管你是新手入门还是想优化项目状态管理,看完心里都有数~
Vue3里说的“Store”,到底指啥?
很多同学看到“Store”第一反应是“状态管理工具”,但Vue3生态里,Store已经从原来的Vuex为主,变成官方推荐Pinia作为首选方案了,简单说,Store是用来集中管理跨组件/页面共享数据的地方。
为啥需要它?比如电商项目里,用户的购物车数据、登录状态,这些信息可能在首页、商品页、结算页都要用,如果每个组件自己存一份,不仅重复代码,数据同步也容易乱,Store就像个“全局数据管家”,把这些共享状态统一存起来,谁要用就来取,改数据也有统一的规则。
Pinia和Vuex的关系得拎清:Vuex是Vue2时代的官方状态管理库,而Pinia可以理解为Vuex的“进化版”——它由Vuex核心团队成员开发,更轻量、API更简洁,还天生适配Vue3的Composition API,现在Vue3项目里,除非维护老项目,否则优先选Pinia~
为啥Vue3官方推荐Pinia,而不是继续用Vuex?
这得从开发体验、性能、生态适配这几点唠:
- API更简单,学习成本低:Vuex里有State、Mutation、Action、Getter、Module这些概念,还得处理命名空间;Pinia直接把Mutation砍掉了,用Actions既能同步也能异步改状态,代码少了一大截,比如修改状态,Vuex得写
commit('mutationName'),Pinia里直接store.count++或者在actions里改,更直观。 - 对Composition API更友好:Vue3主推Composition API,Pinia的
defineStore可以和setup语法无缝配合,你可以把Store里的逻辑拆成组合式函数,复用性拉满。 - Tree-shaking友好,体积更小:Pinia的代码是按需打包的,没用的功能不会被打包进项目,比Vuex轻量很多,对于追求性能的项目,这点太香了。
- TypeScript支持拉满:写TypeScript项目时,Pinia的类型推导几乎“零配置”,定义State时能自动推断类型;Vuex想做好类型提示,得写一堆额外代码,很麻烦。
从零开始,怎么在Vue3项目里搭Store?
实战步骤走一遍,你跟着做就会了~
第一步:安装Pinia
打开终端,进项目目录,执行:
npm install pinia```
#### 第二步:创建Store实例并挂载
在项目的入口文件(main.js`)里,引入并注册Pinia:
```js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const store = createPinia() // 创建Pinia实例
app.use(store) // 挂载到Vue应用
app.mount('#app')
第三步:定义第一个Store模块
新建一个文件(比如src/store/counter.js),用defineStore定义Store:
import { defineStore } from 'pinia'
// defineStore的第一个参数是唯一ID,保证整个应用里不重复
export const useCounterStore = defineStore('counter', {
// state:存共享数据
state: () => ({
count: 0,
userInfo: { name: '默认名', age: 18 }
}),
// getters:对state做计算(类似组件的computed)
getters: {
doubleCount: (state) => state.count * 2,
// 也能通过this访问state,但要注意类型推导(加return)
userGreet: function() {
return `你好,${this.userInfo.name}`
}
},
// actions:处理同步/异步逻辑,修改state
actions: {
increment() {
this.count++
},
// 异步示例:模拟请求数据
async fetchUserInfo() {
const res = await fetch('https://xxx/api/user')
const data = await res.json()
this.userInfo = data // 直接修改state
}
}
})
第四步:在组件里用Store
在Vue组件的setup中,用useStore函数拿到Store实例:
<template>
<div>
<p>当前计数:{{ store.count }}</p>
<p>双倍计数:{{ store.doubleCount }}</p>
<button @click="store.increment">+1</button>
<button @click="fetchUser">获取用户信息</button>
</div>
</template>
<script setup>
import { useCounterStore } from '../store/counter.js'
const store = useCounterStore() // 拿到Store实例
const fetchUser = () => {
store.fetchUserInfo() // 调用actions里的异步方法
}
</script>
Store里的State,修改时有啥讲究?
State是Store存数据的地方,但修改方式得注意“响应式”和“逻辑内聚”:
直接修改VS Actions里改?
Pinia里允许直接修改state(比如store.count++),但如果是复杂逻辑(比如异步请求后改数据,或者多个state联动),建议把逻辑放到actions里,这么做有两个好处:
- 代码可维护:所有修改逻辑集中在actions,后期改需求时,不用满组件找哪里改了state;
- 调试友好:用Pinia DevTools(Chrome插件)能看到actions的调用记录,直接改state的话,调试时不好追踪。
响应式怎么保证?
Vue的响应式靠的是Proxy,所以修改state时,要遵循“响应式规则”:
-
直接改对象属性:
store.userInfo.name = '新名字'✔️(因为userInfo是响应式对象,改属性会触发更新); -
直接替换整个对象/数组:
store.userInfo = { name: '新', age: 20 }✔️(Pinia会自动处理响应式); -
但如果是给对象新增属性,得用
$patch(类似Vuex的patch):// 错误示例:直接新增属性不会触发响应式 store.userInfo.gender = '男' // 正确做法:用$patch store.$patch({ userInfo: { ...store.userInfo, gender: '男' } }) // 或者用函数式$patch(适合复杂修改) store.$patch((state) => { state.userInfo.gender = '男' state.count += 1 })
怎么重置State?
有时候需要把state恢复成初始值(比如用户登出后清空数据),Pinia给每个Store实例提供了$reset方法:
const store = useCounterStore()
store.$reset() // 执行后,count回到0,userInfo回到初始的{ name: '默认名', age: 18 }
Getters在Store里扮演啥角色?怎么写更顺手?
Getters就像Store的“计算属性”,用来基于State生成新数据,而且会缓存结果(只有依赖的State变了,才会重新计算)。
基础用法:依赖State
比如前面的doubleCount,依赖count,所以count变了,doubleCount才会重新计算:
getters: {
doubleCount: (state) => state.count * 2
}
进阶:依赖其他Getters
Getters里可以通过this访问其他Getters(注意用普通函数,别用箭头函数,否则this指向不对):
getters: {
doubleCount: (state) => state.count * 2,
tripleCount() {
return this.doubleCount + this.count // 依赖doubleCount和count
}
}
给Getters传参?
Getters本身是函数,但直接返回函数的话,会失去缓存(因为每次调用都算新函数),如果需要传参,建议封装成“带参数的Getters”:
getters: {
getUserByAge() {
return (targetAge) => {
// 假设state里有users数组:[{name: 'A', age: 20}, {name: 'B', age: 25}]
return this.users.filter(user => user.age === targetAge)
}
}
}
// 组件里用:
store.getUserByAge(20) // 拿到age为20的用户
这种写法虽然没缓存,但适合需要动态筛选数据的场景~
Actions和Mutation有啥区别?Vue3里咋用Actions?
Vuex里Mutation是同步改State,Action是异步+提交Mutation;但Pinia里没有Mutation,Actions既可以同步也可以异步改State,相当于把两者的功能合并了。
同步Actions示例(替代Mutation)
actions: {
increment() {
this.count++ // 直接改state,同步操作
}
}
异步Actions示例(处理网络请求)
actions: {
async login(userInfo) {
const res = await api.login(userInfo)
if (res.code === 200) {
this.user = res.data.user // 存用户信息到state
this.isLogin = true // 改登录状态
}
}
}
为啥Pinia要去掉Mutation?
核心原因是减少概念复杂度,Mutation存在的意义是“强制同步改State,方便调试”,但实际开发中,很多人觉得写Mutation+Action太繁琐,Pinia通过Actions同时支持同步/异步,还能在DevTools里追踪操作,既简化了API,又没丢调试能力~
项目大了,多个Store模块咋组织?
当项目有用户模块、购物车模块、商品模块时,每个模块单独写一个defineStore,就是天然的“模块划分”。
示例:用户模块和购物车模块
src/store/user.js:
export const useUserStore = defineStore('user', {
state: () => ({ token: '', userInfo: {} }),
actions: { async login() {} }
})
src/store/cart.js:
import { useUserStore } from './user.js'
export const useCartStore = defineStore('cart', {
state: () => ({ goodsList: [] }),
actions: {
async addGoods(goodsId) {
const userStore = useUserStore()
if (!userStore.token) {
await userStore.login() // 调用user模块的登录方法
}
// 然后发请求添加商品到购物车
}
}
})
这种“模块间通过useStore调用”的方式,比Vuex的模块命名空间简单太多,不用再纠结rootState、namespace这些东西~
Store和Composition API咋结合更丝滑?
Vue3的Composition API强调“逻辑复用”,Store和它结合能玩出很多花样:
抽离Store逻辑到组合式函数
比如把“用户登录、登出、获取信息”的逻辑抽成useUserLogic.js:
import { useUserStore } from '../store/user.js'
import { onMounted } from 'vue'
export function useUserLogic() {
const userStore = useUserStore()
// 封装登录方法
const handleLogin = async (form) => {
await userStore.login(form)
// 登录成功后的其他逻辑,比如跳转页面
}
// 封装登出方法
const handleLogout = () => {
userStore.$reset() // 清空用户状态
// 其他登出逻辑,比如清除token
}
// 组件挂载时自动获取用户信息
onMounted(() => {
if (userStore.token) {
userStore.fetchUserInfo()
}
})
return { handleLogin, handleLogout }
}
然后在组件里直接用:
<script setup>
import { useUserLogic } from '../composables/useUserLogic.js'
const { handleLogin, handleLogout } = useUserLogic()
</script>
这样组件里只关心“调用方法”,逻辑全在组合式函数里,复用性和可读性都提升了~
生产环境用Store,这些优化点要注意!
项目上线后,性能和可维护性很重要,这几个技巧能帮你少踩坑:
按需引入,利用Tree-shaking
Pinia本身支持Tree-shaking,但如果你的Store文件里有很多逻辑,建议按功能拆分Store(比如把用户、购物车、订单分成不同文件),这样打包时只会把用到的Store打包进去。
避免不必要的响应式
如果有些数据不需要响应式(比如纯工具类的配置信息),可以不用放State里,直接导出普通对象:
// 非响应式配置,直接导出
export const appConfig = {
apiBaseUrl: 'https://xxx.com/api',
theme: 'light'
}
// 组件里用:import { appConfig } from '../config.js'
用DevTools监控状态
Pinia官方有DevTools扩展(Chrome商店搜Vue DevTools,支持Pinia),能实时看State变化、Actions调用记录,生产环境前用它排查状态不同步的问题,效率翻倍~
这些Store常见错误,你踩过吗?
最后避坑环节,分享几个新手常犯的错:
错误1:State修改后页面不更新
原因:可能是直接修改了非响应式数据,或者用了箭头函数导致this指向错误。
解决:
- 对象/数组新增属性用
$patch; - Getters和Actions里用普通函数,别用箭头函数(否则this不是Store实例)。
错误2:多个组件用Store,数据不同步
原因:没正确使用useStore,比如在setup外调用。
解决:必须在setup函数或组合式函数里调用useStore,因为Pinia是基于Vue的依赖注入,只有在组件上下文里才能拿到正确的Store实例。
错误3:TypeScript类型报错
原因:State的类型没正确推导,或者actions参数类型不对。
解决:
- 定义State时,尽量用字面量初始化(比如
state: () => ({ count: 0 })),让TypeScript自动推导; - 给actions的参数加类型注解:
async login(userInfo: UserInfo) {}。
写到这,Vue3 Store(Pinia)的核心知识点和实战技巧差不多覆盖全了,Store的本质是“管理共享状态”,别为了用而用——如果组件间数据传递用Props/Events、Provide/Inject能解决,就没必要上Store,但遇到复杂的跨组件状态同步、异步逻辑管理,Pinia绝对是高效利器~ 现在可以动手在自己项目里搭个Store试试,把登录状态、购物车这些场景练一遍,很快就能上手啦~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。