本文作者为奇舞团前端研发工程师
从Vuex到Pinia
一. 概述
在开发Vue项目时,我们一般使用Vuex
来进行状态管理,但是在使用Vuex
时始终伴随着一些痛点。比如:需要使用Provide/Inject
来定义类型化的InjectionKey
以便支持TypeScript
,模块的结构嵌套、命名空间以及对新手比较难理解的流程规范等。Pinia
的出现很好的解决了这些痛点。本质上Pinia
也是Vuex
团队核心成员开发的,在Vuex
的基础上提出了一些改进。与Vuex
相比,Pinia
去除了Vuex
中对于同步函数Mutations
和异步函数Actions
的区分。并且实现了在Vuex5
中想要的大部分内容。
二.使用
在介绍Pinia
之前我们先来回顾一下Vuex
的使用流程
1.Vuex
Vuex
是一个专为Vue.js
应用程序开发的状态管理库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它主要用来解决多个组件状态共享的问题。

主流程: 在Store中创建要共享的状态state,修改state流程:
Vue Compontents
dispatch Actions(在Actions中定义异步函数),Action commit Mutations,在Mutations中我们定义直接修改state的纯函数,state修改促使Vue compontents 做响应式变化。
(1) 核心概念
State
: 就是组件所依赖的状态对象。我们可以在里面定义我们组件所依赖的数据。可以在Vue组件中通过this.$store.state.xxx
获取state里面的数据.Getter
:从store
中的state
派生出的一些状态,可以把他理解为是store
的计算属性.Mutation
:更改store
中状态的唯一方法是提交 mutation,我们通过在mutation
中定义方法来改变state里面的数据.
在Vue组件中,我们通过
store.commit('方法名')
,来提交mutation
。需要注意的是,Mutation 必须是同步函数。
Action:
action类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态.
Action 可以包含任意异步操作.
Module
: 当我们的应用较大时,为了避免所有状态会集中到一个比较大的对象中,Vuex
允许我们将 store 分割成模块(module),你可以把它理解为Redux
中的combineReducer
的作用.
(2) 在组合式API中对TypeScript
的支持
在使用组合式API编写Vue
组件时候,我们希望使用useStore
返回类型化的store
,流程大概如下:
定义类型化的
InjectionKey
。将
store
安装到Vue
应用时提供类型化的InjectionKey
。将类型化的
InjectionKey
传给useStore
方法并简化useStore
用法
// store.ts
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { InjectionKey } from 'vue'
export interface IState {
count: number
}
// 1.定义 injection key
export const key: InjectionKey<Store<IState>> = Symbol()
export default createStore<IState>({
state: {
count: 0
},
mutations: {
addCount (state:IState) {
state.count++
}
},
actions: {
asyncAddCount ({ commit, state }) {
console.log('state.count=====>', state.count++)
setTimeout(() => {
commit('addCount')
}, 2000)
}
},
})
// 定义自己的 `useStore` 组合式函数
export function useStore () {
return baseUseStore(key)
}
main.ts
import { createApp } from 'vue'
import { store, key } from './store'
const app = createApp({ ... })
// 传入 injection key
app.use(store, key)
app.mount('#app')
组件中使用
<script lang="ts">
import { defineComponent, toRefs } from 'vue'
import { useStore } from '../store'
export default defineComponent({
setup () {
const store = useStore()
const clickHandel = () => {
console.log('====>')
store.commit('addCount')
}
const clickAsyncHandel = () => {
console.log('====>')
store.dispatch('asyncAddCount')
}
return {
...toRefs(store.state),
clickHandel,
clickAsyncHandel
}
}
})
</script>
Pinia 的使用

基本特点
Pinia
同样是一个Vue的状态管理工具,在Vuex
的基础上提出了一些改进。与vuex相比,Pinia
最大的特点是:简便。
它没有
mutation
,他只有state
,getters
,action
,在action
中支持同步与异步方法来修改state
数据类型安全,与
TypeScript
一起使用时具有可靠的类型推断支持模块化设计,通过构建多个存储模块,可以让程序自动拆分它们。
非常轻巧,只有大约 1kb 的大小。
不再有
modules
的嵌套结构,没有命名空间模块Pinia
支持扩展,可以非常方便地通过本地存储,事物等进行扩展。支持服务器端渲染
安装与使用
安装
yarn add pinia
# 或者使用 npm
npm install pinia
核心概念:
store
: 使用defineStore()
函数定义一个store,第一个参数是应用程序中store的唯一id. 里面包含state
、getters
和 actions
, 与Vuex相比没有了Mutations
.
export const useStore = defineStore('main', {
state: () => {
return {
name: 'ming',
doubleCount: 2
}
},
getters: {
},
actions: {
}
})
注意:store 是一个用reactive 包裹的对象,这意味着不需要在getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构.
export default defineComponent({
setup() {
const store = useStore()
// ❌ 这不起作用,因为它会破坏响应式
// 这和从 props 解构是一样的
const { name, doubleCount } = store
return {
// 一直会是 "ming"
name,
// 一直会是 2
doubleCount,
// 这将是响应式的
doubleValue: computed(() => store.doubleCount),
}
},
})
当然你可以使用computed
来响应式的获取state的值(这与Vuex中需要创建computed
引用以保留其响应性类似),但是我们通常的做法是使用storeToRefs
响应式解构Store.
const store = useStore()
// 正确的响应式解构
const { name, doubleCount } = storeToRefs(store)
State
: 在Pinia中,状态被定义为返回初始状态的函数.
import { defineStore } from 'pinia'
const useStore = defineStore('main', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
// 所有这些属性都将自动推断其类型
counter: 0,
name: 'Eduardo'
}
},
})
组件中state的获取与修改:
在Vuex
中我们修改state
的值必须在mutation
中定义方法进行修改,而在pinia
中我们有多中修改state的方式.
基本方法:
const store = useStore()
store.counter++
重置状态:
const store = useStore()
store.$reset()
使用
$patch
修改state [1] 使用部分state
对象进行修改
const mainStore = useMainStore()
mainStore.$patch({
name: '',
counter: mainStore.counter++
})
[2] $patch
方法也可以接受一个函数来批量修改集合内部分对象的值
cartStore.$patch((state) => {
state.counter++
state.name = 'test'
})
替换state 可以通过将其 $state 属性设置为新对象,来替换
Store
的整个状态:
mainStore.$state = { name: '', counter: 0 }
访问其他模块的
state
Vuex
中我们要访问其他带命名空间的模块的state我们需要使用rootState
addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
/// 通过rootState 访问main的数据
console.log('rootState.main.count=======', rootState.main.count)
if (state.tabLists.some(item => item.id === tab.id)) { return }
setTimeout(() => {
state.tabLists.push(tab)
}, 1000)
},
Pinia 中访问其他
store
的state
import { useInputStore } from './inputStore'
export const useListStore = defineStore('listStore', {
state: () => {
return {
itemList: [] as IItemDate[],
counter: 0
}
},
getters: {
},
actions: {
addList (item: IItemDate) {
this.itemList.push(item)
///获取store,直接调用
const inputStore = useInputStore()
inputStore.inputValue = ''
}
})
Getter: Getter完全等同于Store状态的计算值.
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// 自动将返回类型推断为数字
doubleCount(state) {
return state.counter * 2
},
// 返回类型必须明确设置
doublePlusOne(): number {
return this.counter * 2 + 1
},
},
})
如果需要使用
this
访问到 整个store
的实例,在TypeScript
需要定义返回类型. 在setup()
中使用:
export default {
setup() {
const store = useStore()
store.counter = 3
store.doubleCount // 6
},
}
访问其他模块的getter
对于
Vuex
而言如果要访问其他命名空间模块的getter
,需要使用rootGetters
属性
/// action 方法
addAsyncTabs ({ state, commit, rootState, rootGetters }:ActionContext<TabsState, RootState>, tab:ITab): void {
/// 通过rootGetters 访问main的数据
console.log('rootGetters[]=======', rootGetters['main/getCount'])
}
Pinia
中访问其他store中的getter
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
Action:actions 相当于组件中的methods,使用defineStore()
中的 actions 属性定义.
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
pinia
中没有mutation
属性,我们可以在action
中定义业务逻辑,action
可以是异步的,可以在其中await 任何 API调用甚至其他操作.
...
//定义一个action
asyncAddCounter () {
setTimeout(() => {
this.counter++
}, 1000)
}
...
///setup()中调用
export default defineComponent({
setup() {
const main = useMainStore()
// Actions 像 methods 一样被调用:
main.asyncAddCounter()
return {}
}
})
访问其他store中的Action
要使用另一个 store中的action ,可以直接在操作内部使用它:
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
// ...
}),
actions: {
async fetchUserPreferences(preferences) {
const auth = useAuthStore()
///调用其他store的action
if (auth.isAuthenticated()) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
在Vuex
中如果要调用另一个模块的Action
,我们需要在当前模块中注册该方法为全局的Action
,
/// 注册全局Action
globalSetCount: {
root: true,/// 设置root 为true
handler ({ commit }:ActionContext<MainState, RootState>, count:number):void {
commit('setCount', count)
}
}
在另一个模块中对其进行dispatch
调用
/// 调用全局命名空间的函数
handelGlobalAction ({ dispatch }:ActionContext<TabsState, RootState>):void {
dispatch('globalSetCount', 100, { root: true })
}
三. 总结
与 Vuex[1] 相比,Pinia[2] 提供了一个更简单的 API,具有更少的操作,提供Composition API
,最重要的是,在与TypeScript
一起使用时具有可靠的类型推断支持,如果你正在开发一个新项目并且使用了TypeScript
,可以尝试一下pinia
,相信不会让你失望。
参考资料
[1]
Vuex: https://vuex.vuejs.org/zh/#什么是"状态管理模式"?
[2]Pinia: https://pinia.web3doc.top/core-concepts/
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。