vue状态管理之Vuex

279 阅读8分钟

一、前言

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)
}