或者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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。