mutations
用于更改 Vuex 的 store 中的状态。每个 mutation 都有一个字符串的 事件类型 (type) 和一个 回调函数 (handler) 。并且回调函数中,可以有以下值。
state
为了修改state中的状态,肯定要传入state啊。
payload
(这个可以自己命名) 用于提供修改状态的数据,他是当我们调用commit函数传递的值。并且可以传入任意数据。
const store = createStore({ mutations: { changeName (state, payload) { state.name = payload.name } } })
上面定义了mutation,如何在项目中触发呢?
- 在template中, 通过
$store.commit(对应的mutation名, payload)
即可。
- 在optionsAPI中,通过
this.$store.commit(对应的mutation名, payload)
即可。
<button @click="changeName">改变state.name: {{$store.state.name}}</button> methods: { changeName() { // this.$store.commit('changeName', {name: 'llm'}) this.$store.commit({ type: 'changeName', name: 'llm', }) }, },
- 在optionsAPI中,通过vuex提供的
useStore
API即可。
<button @click="changeName1">改变state.name: {{$store.state.name}}</button> setup() { const store = useStore() const changeName1 = () => { store.commit('changeName', { name: 'llm', }) } return { changeName1 } },
从上面的代码示例中,我们可以看出,提交commit有两种方法。
- 先传入对应的mutation名,在传入对应的数据
- 直接传入一个对象,且对象中定义一个
type
属性来指定mutation的名相应的,vuex也提供了对应的map函数。mapMutations
。 终于这次不需要我们自己封装hook,vue2和vue3中使用mapMutations
行为是一样的。因为当我们结构mapMutations
返回的对象,他们本身就是一个个函数,我们直接调用,并传入对应的payload。
// 我们只需要在template中触发对应的method时传入对应的payload <button @click="updateName({name: 'llm'})">改变state.name: {{$store.state.name}}</button> <button @click="updateName({type: 'changeName', name: 'jcl'})">改变state.name: {{$store.state.name}}</button> methods: { ...mapMutations(['changeName']), ...mapMutations({ updateName: "changeName" }) }, setup() { const storeMutations = mapMutations(['changeName']) return { ...storeMutations } }
一条重要的原则就是要记住 mutation 必须是同步函数。这是因为devtool工具会记录mutation的日记。每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照。但是在mutation中执行异步操作,就无法追踪到数据的变化,所以Vuex的重要原则中要求 mutation必须是同步函数。
actions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。变更状态依旧是在mutations中。
- Action 可以包含任意异步操作。 actions中的回调函数第一个参数不再是state,而是一个store对象并且增加了rootState和rootDetters属性,一般命名为context。它里面存在着以下属性。
commit
, 用于提交mutation,触发mutations中对应的回调函数。
dispatch
, 我们可能还会触发对应的actions回调函数。
getters
, 为了获取当前模块中的getters。
rootGetters
, 为了获取根模块的getters。当我们设置了modules
选项时,就会有用。
rootState
, 为了获取根模块的state。当我们设置了modules
选项时,就会有用。
state
, 获取当前模块的state。 actions回调函数还可以传入第二个参数,让用户调用dispatch时,传入的数据。
下面我们就来看看如何使用actions吧。
const store = createStore({ mutations: { changeAge (state, payload) { state.age = payload.age } } actions: { changeAge (context, payload) { // 异步提交commit setTimeout(() => { context.commit('changeAge', payload) }, 1000); }, })
上面定义了action,如何在项目中触发呢?
- 在template中, 通过
$store.dispatch(对应的action名, payload)
即可。
- 在optionsAPI中,通过
this.$store.dispatch(对应的dispatch名, payload)
即可。
<button @click="changeAge">异步改变state.age {{$store.state.age}}</button> methods: { changeAge() { this.$store.dispatch('changeAge', { age: 999, }) }, }
- 在optionsAPI中,通过vuex提供的
useStore
API即可。
<button @click="changeAge1">异步改变state.age {{$store.state.age}}</button> setup() { const store = useStore() const changeAge1 = () => { store.dispatch('changeAge', { age: 1000, }) } return { changeAge1 } }
同样,vuex也提供了对应的map方法,mapActions
。使用同mapMutions
。
methods: { ...mapActions(['changeAge']), ...mapActions({ updateAge: "changeAge" }) } setup() { const actions = mapActions(['changeAge']) const actions2 = mapActions({ updateAge: "changeAge" }) return { ...actions, ...actions2 } }
重点:当我们通过dispatch分发action时,我们可以结合async await 给action回调返回一个promise对象。用于在组件中知道异步请求的状态。
modules
什么是Module?
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
具体用法请看官网:next.vuex.vuejs.org/zh/guide/mo…
下面介绍一下使用事项:
- 获取子模块中的state数据
$store.state.模块名.数据
$store.state.home.homeCounter
- 模块中的getters中的回调可以接受4个参数,分别是
state
,getters
,rootState
,rootGetters
。可以获取当前模块中的state, getters, 父模块中的state, getters。
- 我们一般使用模块的时候,都需要在定义模块的时候加上
namespaced: true
。不然模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。
下面测试,如果我们不加命名空间时
<h1>{{$store.state.rootCounter}}</h1> <h1>{{$store.state.home.homeCounter}}</h1> <button @click="increment">触发increment commit函数</button> methods: { increment() { this.$store.commit('increment') } }
他会触发子模块和根模块中的对应的mutations回调函数。 但是我们加上命名空间时,就不会出现这样的问题。因为vuex内部会自动将触发回调的函数名改成子模块/对应的mutation名
。
<h1>rootCounter: {{$store.state.rootCounter}}</h1> <h1>homeCounter: {{$store.state.home.homeCounter}}</h1> <button @click="increment">触发increment commit函数</button> <button @click="homeIncrement">触发increment commit函数</button> methods: { homeIncrement() { this.$store.commit('home/increment') }, increment() { this.$store.commit('increment') }, },
- 当我们触发action时,基本和mutation是一样的。只是我们在触发子模块对应的action的时候,可以指定也触发父模块同名的action。
<h1>rootCounter: {{$store.state.rootCounter}}</h1> <h1>homeCounter: {{$store.state.home.homeCounter}}</h1> <button @click="homeIncrementAction">触发increment dispatch函数</button> actions: { incrementAction ({ commit, dispatch, state, rootState, getters, rootGetters }) { // 指定触发当前模块的increment mutation commit("increment") // 指定触发父模块的increment mutation commit("increment", null, { root: true }) } } methods: { homeIncrementAction() { this.$store.dispatch('home/incrementAction') } }
- 如果想要在项目中通过对应的map方法,来映射对应模块中的state, getters, mutations, actions我们有很多方法。但是这种方式是比较方便的。
createNamespacedHelpers(namespace)
, 创建基于命名空间的组件绑定辅助函数。其返回一个包含mapState
、mapGetters
、mapActions
和mapMutations
的对象。它们都已经绑定在了给定的命名空间上。
在optionsAPI中使用都是没有问题的。下面我们基于上面开发的hook函数来看在compositionAPI中的使用。
修改useState hook
import { useStore, mapState, createNamespacedHelpers} from 'vuex' import { computed } from 'vue' const useState = function(mapper, moduleName) { // mapper: Array | Object const store = useStore() // 先将vuex提供的mapState赋值给mapperFn,它用于映射对应模块的store.state let mapperFn = mapState // 看其是否传入了moduleName, 没有就是使用默认的mapState if (typeof moduleName === 'string' && moduleName.length > 0) { mapperFn = createNamespacedHelpers(moduleName).mapState } //将返回一个对象 const storeStateFns = mapperFn(mapper) // 用于存放获取到的state.属性: ref对象 键值对 const storeState = {} Object.keys(storeStateFns).forEach(item => { // 这我们知道辅助函数的内部是通过this.$store来实现的 // setup中没有this, 所以通过bind来改变this的指向 const fn = storeStateFns[item].bind({$store: store}) //将最后的值放在storeState中 storeState[item] = computed(fn) }) return storeState } export default useState
修改useGetters hook
import { useStore, mapGetters, createNamespacedHelpers } from 'vuex' import { computed } from 'vue' const useGetters = (mapper, moduleName) => { const store = useStore() // 先将vuex提供的mapState赋值给mapperFn,它用于映射对应模块的store.state let mapperFn = mapGetters // 看其是否传入了moduleName, 没有就是使用默认的mapState if (typeof moduleName === 'string' && moduleName.length > 0) { mapperFn = createNamespacedHelpers(moduleName).mapGetters } const storeGetterFns = mapperFn(mapper) const storeGetter = {} Object.keys(storeGetterFns).forEach((item) => { const fn = storeGetterFns[item].bind({ $store: store }) storeGetter[item] = computed(fn) }) return storeGetter } export default useGetters
从上面可以看出,两个hook实现逻辑基本一样,只是调用的内部API不同,如果有想法封装一下,自己发挥。
下面就来测试一下
<h1>rootCount_state: {{rootCounter}}</h1> <h1>homeCounter_state: {{homeCounter}}</h1> <h1>doubleHomeCounter_getters: {{doubleHomeCounter}}</h1> <h1>doubleRootCounter_getters: {{doubleRootCounter}}</h1> setup() { return { ...useState(['homeCounter'], 'home'), ...useState(['rootCounter']), ...useGetters(['doubleHomeCounter'], 'home'), ...useGetters(['doubleRootCounter']) } }
// homeModule.js const homeModule = { namespaced: true, state () { return { homeCounter: 1 } }, getters: { doubleHomeCounter (state, getters, rootState, rootGetters) { return state.homeCounter * 2 } } } export default homeModule
//store.js import { createStore } from "vuex" import home from './modules/home' const store = createStore({ state () { return { rootCounter: 100 } }, getters: { doubleRootCounter (state) { return state.rootCounter * 2 } } modules: { home } }); export default store;