一、前言
vue中进行状态管理,目前主要使用的有两种,一个是vuex,一个是pinia,本文主要是介绍一下vuex的相关知识,争取把vuex说明白,希望能给想了解的同学们一点点帮助,也是对自己知识点的一个梳理,有不准确的地方,可以评论区留言讨论。本文的例子使用的是 vuex@4.1.0。
二、基础使用
在开始前,我们先来看一个基础使用的小例子,有一个基础印象
创建store实例
store/index.js
import { createStore, useStore as useStoreCore } from 'vuex'
// 这个key是配合useStore()用的,详细后面会说
export const key = Symbol()
// 创建store实例
const store = createStore({
state: {
count: 0
},
mutations: {
// 修改状态,mutation是唯一能修改state的地方
SET_COUNT(state, count) {
state.count = count
}
},
actions: {
increment({commit, state}) {
commit('SET_COUNT', state.count + 1)
}
}
})
// 这个地方是为了避免组件内每次调用useStore()都要传key,方便一点
export const useStore = () => {
return useStoreCore(key)
}
export default store
vue实例注册
import { createApp } from 'vue'
import App from './App.vue'
import store, { key } from '@/store'
const app = createApp(App)
// 注册
app.use(store, key)
app.mount('#app')
组件中使用
<template>
<div>
<div>{{ count }}</div>
<button @click="incrementHandler">增加</button>
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from '@/store'
export default {
name: 'App',
setup() {
const store = useStore()
const count = computed(() => store.state.count)
const incrementHandler = () => {
// 派发更新状态
store.dispatch('increment')
}
return {
count,
incrementHandler
}
}
}
</script>
上面就是一个最基本的使用,vuex也就是按照这样一个套路来用的。
三、核心概念
盗用一下vuex官方文档的图,我们可以看到,vuex主要由三块构成:state、mutation、action,其中state用来存储数据,mutation用来更改数据,action用来派发mutation,其中state和mutation是必不可少的。
1、state
state可以理解未一个数据仓库,里面存着全局可访问的一个响应式的数据对象,上面例子中,我们在实例化store时,创建了一个state,包含一个count属性,在组件中使用时,只需要引入store实例,通过 store.state.count就可以访问到state中存放的数据,当别的地方改变了这个state中的状态,这里的引用也会更新;
组件中引入state的方式有很多种:
-
通过store实例访问,获取store实例有多种方式
-
通过useStore()钩子,获取store实例访问,如上面的例子
-
通过es模块语法直接导入创建的store实例(import store from '@/store')
-
在vue2语法中通过
this.$store获取store实例 -
通过mapState()获取,这种方式我觉得比较适合在vue2中使用,在setup中使用比较麻烦,还是项目的例子,使用mapState()实现
// 用选项参数方式开发时 import { mapState } from 'vuex'
export default { computed: mapState(['count']) }
// 在setup中使用,需要写的东西就比较多了 import { useStore, mapState} from 'vuex' import { computed } from 'vue' import { useStore } from '@/store' ... setup() { const store = useStore() const stateFn = mapState(['count'])
return { count: computed(stateFn.count.bind({$store: store})) }}
如果一定要在setup中使用mapState(),可以封装一个函数,使用起来倒也方便
import { useStore, mapState} from 'vuex'
import { computed } from 'vue'
import { useStore } from '@/store'
const useState = (keys: string[]) => {
// 使用仓库对象
const store = useStore()
// 根据参数进行映射
const stateFn = mapState(keys)
const res = {}
for (const key in stateFn) {
if (Object.hasOwnProperty.call(stateFn, key)) {
// 修改计算函数内部this指向
const fn = stateFn[key].bind({ $store: store })
// 存储
res[key] = computed(fn)
}
}
// 将映射结果返回
return res
}
export {
useState
}
// 在别的组件中使用
setup() {
return {
...useState(['count'])
}
}
<div>{{ count }}</div>
2、getter
getter也是用来获取state中的值的,一般是用来获取根据state计算得来的值,比如现在有个地方需要获得 state.count * 2这个结果
// 定义getter
createStore({
state: {
count: 0
},
getters: {
doubleCount(state) {
console.log('doubleCount')
reutrn state.count * 2
},
mutations: {},
actions: {}
}
})
// 组件内使用
import { useStore } from '@/store'
setup() {
const store = useStore()
const count1 = computed(() => store.getters.doubleCount)
const count2 = computed(() => store.getters.doubleCount)
return {
count1,
count2
}
}
当组件中访问store.getters.doubleCount时,会执行getter定义中的那个函数,返回计算结果,这里我们访问了两次store.getters.doubleCount ,在getter定义中执行了console.log('doubleCount'),可以发现在两次调用后,只执行了一次计算,这是因为getter中做了缓存,在计算中访问到的state属性没有变化时,下次再访问getter,会直接返回上次计算的值 。
当然,访问getter也有两种方式
- 通过store实例访问,如上面的例子
store.getters.doubleCount,store实例获取的方式,参考state获取 - 使用mapGetters(),实现方式与 mapState()一样的,把上面state里面的封装的mapState换成mapGetters,然后使用的时候,useState()传入getters里面定义的key就可以了
3、mutation
mutation是vuex中改变state的唯一途径,要变更state,必须定义一个mutation,然后commit
createStore({
state: {
count: 0
},
mutations: {
INCREMENT(state) {
return {
...state,
count: state.count + 1
}
}
}
})
// 在组件中执行commit
setup() {
const store = useStore()
// 假设这个是一个按钮绑定的事件函数,每次点击按钮,count都会+1
const clickHandler = () => {
store.commit('INCREMENT')
}
return {
clickHandker
}
}
(1)传递参数
提交mutation时,也可以传递参数,两种方式:
① commit(type: string, params: any)
第一个参数传入mutation的key,第二个参数可以是任意类型的数据
createStore({
state: {
count: 0
},
mutations: {
SET_COUNT(state, count){
state.count = count
}
}
})
// 组件调用
store.commit('SET_COUNT', 10)
② commit(payload:{type: string}&{[key: string]: any})
执行commit时,传入一个对象payload,这个对象包含一个type属性,指向mutation定义,payload其他属性将作为载荷使用
createStore({
state: {
count: 0
},
mutations: {
SET_COUNT(state, payload){
state.count = payload.count
}
}
})
// 组件调用
store.commit({
type: 'SET_COUNT',
count: 10
})
(2)提交方式
提交mutation也有三种方式:
-
通过store实例提交,如例子中
store.commit() -
通过action提交,action函数中第一个参数返回的是一个也store类型的对象context,可以通过
context.commit()提交mutation -
通过mapMutations提交,与mapState获取类似,在setup中操作比通过选项参数实现麻烦一点
createStore({ state: { name: '' }, mutations: { SET_NAME(state, name) { state.name = name } } })
// 通过选项参数实现 export default { methods: { // 通过 this.SET_NAME('李四')提交 ...mapMutations(['SET_NAME']) } }
// 在setup中的实现 import { mapMutations } from 'vuex' import store from '@/store'
setup() { const mutationsFn = mapMutations(['SET_NAME'])
const clickHandler = () => { mutationsFn.SET_NAME.call({$store: store}, '张三') }
return { clickHandler } }
(3)mutations必须是同步的
在官方文档中写了,这个必须是同步的,异步的mutations不利于使用 devtool 调试查看store,每次提交mutation,都会生成一个结果状态的快照,如果mutation内存在异步修改,那么在生成快照时,取到的就不是最新的结果,这里放一个尤大关于这个问题的回答
如果想理解这个异步的问题,最好的方式就是动手打开devtool,写一个异步的mutation调试一番
鼠标移到mutations对应的绿色的点上面,页面上的数据对应的就是这个点提交时的状态,如果这里的mutation是异步的,在生成快照时,mutation还没有去改变state,那快照必然与预期不符。
所以,异步的mutation仅仅是不利于调试工具,并不影响实际效果(有多少人会用devtool去查看数据状态)
4、action
action是用来提交mutation的,在action内可以进行任何异步操作,每个action函数接收两个参数,一个类似store实例的context,一个是外部dispatch时传入的任意参数
createStore({
state: {
name: ''
},
mutations: {
SET_NAME(state, name) {
state.name = name
}
},
actions: {
updateName(context, name) {
context.commit('SET_NAME', name)
}
}
})
组件中调用
store.dispatch('updateName', '张三')
(1)action分发
分发一个action有两种方式:
-
通过store实例调用dispatch,如上面的例子;
-
通过 mapActions(),和前面的mapMutations类型
createStore({ state: { name: '' }, mutations: { SET_NAME(state, name) { state.name = name } }, actions: { setName({commit}, name) { commit('SET_NAME', name) } } })
// 选项参数方式使用mapActions export default { methods: { ...mapActions(['setName']) } }
this.setName('张三')
// 在setup中使用 mapActions import { mapActions } from 'vuex import { useStore } from '@store'
setup() { const store = useStore() const actionsFn = mapActions(['setName'])
const clickHandler = () => { actionsFn.setName.call({$store: store}, '张三') }
return { clickHandler } }
(2)action中的异步
action的定义支持返回Promise对象,store.dispatch()返回的结果也是一个Promise对象,能够获取到action内的返回值,所以action内的异步操作还是很方便的
createStore({
state: {
name: ''
},
mutations: {
SET_NAME(state, name) {
state.name = name
}
},
actions: {
setName({commit}) {
return new Promise((resolve) => {
setTimeout(() => {
commit('SET_NAME', '张三')
resolve(true)
}, 2000)
})
}
}
})
store.dispatch('setName').then(res => {
// res 就是action中resolve返回的值
console.log(res)
})
5、module
前面我们学到的所有的状态,都是在一个state上保存的,mutation、action也都是在一起,当项目变得复杂,状态保存的结构越复杂,这种单一的state是不利于维护的,这时候,就可以使用module来进行状态的管理;
每个module都维护这一套state、getters、mutation、action
const userModule = {
state: {
name: '',
age: 0
},
mutations: {
// 这里的state是当前这个module的state,不与其他模块冲突
SET_NAME(state, name) {
state.name = name
},
SET_AGE(state, age) {
state.age = age
}
},
getters: {
// 这里的state也是当前模块的state
doubleName(state, getters, rootState, rootGetters) {
return state.name + state.name
}
},
actions: {
setName({state, commit, rootState, rootGetters, dispatch }, name) {
// 提交当前module的SET_NAME
commit('SET_NAME', name)
// 提交其他mudole的mutation,menus模块启用了命名空间,所以这里提交mutation时加上了menus
commit('menus/SET_MENU', [], { root: true })
// 分发当前模块其他action
dispatch('setAge', 10)
// 分发其他模块的action
dispatch('menus/setMenus', [], { root: true })
},
setAge({commit}, age) {
commit('SET_AGE', age)
}
}
}
const menuModule = {
namespaced: true,
state: {
menus: []
},
mutations: {
SET_MENU(state, menus) {
state.menus = menus
}
},
actions: {
setMenus({commit}, menus) {
commit('SET_MENU', menus)
}
}
}
createStore({
state: {
count: 0
},
mutations: {
SET_COUNT(state, count) {
state.count = count
}
},
modules: {
user: userModule,
menus: menuModule
}
})
// 组件中使用
const store = useStore()
// 获取,注意这里是通过state.user下获取的
const name = store.state.user.name
// 通过getters获取
const dbName = store.getters.doubleName
// 更改
store.commit('SET_NAME', '张二河')
上面这种就是一个简单的module,将user相关的状态都在这个module中操作,不与其他的状态干扰。
(1)模块内的局部状态
上面的例子中,展示了模块内的状态的操作,有以下几个注意点:
- mutation、getters、action中的state取到的都是当前这个mudole的state;
- getters和action还支持取到rootState和rootGetters,这样也就可以在当前module内获取到其他module的状态
- 如果要在action中执行其他module的mutation,需要给commit()传入第三个参数
{root: true},执行其他module中的action也是在dispatch()传入第三个参数{root: true}
(2)模块的命名空间
上面的例子,module虽然单独定义了,但是mutation、getters、action在被使用时,没有模块的限制,就有可能在不同模块定义了相同key的mutation、getters、action,vuex给模块提供了一个namespaced属性,设置为true后,在使用时有些许区别
const userModule = {
namespaced: true,
state: {
name: ''
},
mutations: {
SET_NAME(state, name) {
state.name = name
}
},
getters: {
fullName(state) {
return '李' + state.name
}
},
actions: {
setName({commit}, name) {
commit('SET_NAME', name)
}
}
}
createStore({
modules: {
user: userModule
}
})
// 组件中使用,需要加上模块名
const fullName = store.getters['user/fullName']
store.commit('user/SET_NAME', '四')
store.dispatch('user/setName', '四')
四、状态持久化
这个主要是使用第三方的库,我都是用vuex-persistedstate,使用比较简单,这里我只列出几个比较常用的配置项,其他的配置项可自行查阅文档
import createPersistedState from "vuex-persistedstate"
createStore({
// state、mutation、action这些都不写了
modules: {
user: userModule,
menus: menusModule
},
plugins: [
createPersistedState({
// 本地储存名的key
key: 'test',
// 本地存储模式,默认是 localStorage
storage: window.sessionStorage,
// 指定模块,这里只对user模块进行本地存储
paths: ['user']
})
]
})
五、useStore中的key
在store实例注册到vue上时,我们需要传入一个InjectionKey ,这个key必须是唯一的,在组件内使用useStore()获取store时,需要传入这个key
import { useStore, createApp } from 'vuex
import { createApp } from 'vue'
const key = Symbol()
const store = createStore({
...
})
const app = createApp(App)
// 将store注册到vue上,第二个参数传入这个InjectionKey
app.use(store, key)
app.mount('#app')
// 组件中使用
const store = useStore(key)
(1)key的作用
在调用app.use(store, key)时,vuex内部定义的install函数,会调用vue.provide(injectionKey, store),将store实例透传到组件中。而useStore()这个钩子内部,就是调用了inject(injectKey),获取跟组件透传下来的store实例,这个过程中,这个 injectKey 作为透传的标识存在。
(2)useStore() 的简便使用
每次调用useStore()都需要传入这个injectKey,比较麻烦,推荐进行一个简单的包装,如前面基础使用中的第一个例子
import { useStore as useStoreCore } from 'vuex
export const key = Symbol()
export const useStore = () => {
return useStoreCore(key)
}
在组件中只需要导入这个自定义的useStore()即可
六、typescript中使用
vuex对ts的支持一言难尽啊....
为了在项目中更舒服的使用vuex,需要将vuex各个模块的state定义一下类型,直接贴代码了
import { InjectionKey } from 'vue'
import { createStore, useStore as useStoreCore, Store } from 'vuex'
// 这个是根实例上的state
export interface RootStateTypes {
count: number
}
// 这个是user模块上的state
export interface UserStateTypes {
name: string
}
// 这个是给useStore()用的,使得返回的store能拿到完整的state类型
export interface BaseStateTypes extends RootStateTypes {
user: UserStateTypes
}
export const key: InjectionKey<Store<RootStateTypes>> = Symbol()
const userModule: Module<UserStateTypes, RootStateTypes> = {
state: {
name: ''
},
}
const store = createStore<RootStateTypes>({
state: {
count: 0
},
modules: {
user: userModule
}
})
export const useStore = () => {
return useStoreCore<BaseStateTypes>(key)
}