Vuex 核心概念(一):State,Getters,Mutations,Actions

164 阅读4分钟

Vuex 是什么

Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态。

Vuex 的核心就是 store(仓库),store 就是一个容器,它用来保存项目中的状态(数据)。

Vuex 的状态存储是响应式的。当我们获取到了 store 中的状态,若 store 中的状态发生了变化,那么相应的组件也会被更新。

如果我们希望修改 store 中的状态,只能通过提交 mutation 来改变,而不能直接对状态进行修改。

this.$store.commit('increment') // 合法

this.$store.state.counter++ // 不合法

Vuex 的基本使用

计数案例

把数据放在组件中管理

<template>
  <div>
    <div>当前计数:{{ counter }}</div>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>
export default {
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    increment() {
      this.counter++
    },
    decrement() {
      this.counter--
    }
  }
}

通过 vuex 来管理数据

如果我们通过 vuex 中来管理数据的话,步骤如下:

  • 首先创建一个 store 实例,这个 store 实例提供一个 state 对象和一些 mutation

  • 然后将 store 实例挂载到根组件上,这样在其他组件中我们就可以通过 this.$store 获取到这个 store 实例。

  • options api 中通过 this.$store.state 获取到状态对象,并通过 this.$store.commit 方法触发状态变更。

  • setup 函数中,不能直接通过 this.$store 获取到 store 实例,需要通过 useStore 方法来获取。

代码如下:

import { createStore } from 'vuex'

// 1. 创建仓库,提供一个 state 对象和一些 mutation
// state 对象用来管理状态
// mutation 用来修改 state 中的状态,通过提交方法的方式
const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state) {
      state.counter++
    },
    decrement(state) {
      state.counter--
    }
  }
})

const app = createApp(App)

// 2. 把仓库挂载到根组件上
app.use(store)

在组件中:

<template>
  <div>
    <!-- 在模板中可以通过 $store.state 来获取到 state 对象 -->
    <div>当前计数:{{ $store.state.counter }}</div>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>
// Vue2
export default {
  methods: {
    increment() {
      this.$store.commit('increment')
    },
    decrement() {
      this.$store.commit('decrement')
    }
  }
}
// Vue3
import { useStore } from 'vuex'

export default {
 setup() {
    const $store = useStore()

    return {
      $store
    }
  }
}

State

单一状态树/单一数据源

所有的状态都保存在一个 store 中,也就是说一个项目中只有一个 store 实例。

在组件中获取 Vuex 状态

在实际开发中,如果需要多次获取,一般是跟计算属性结合使用。(这里涉及到 Vue3 中计算属性的使用方法,具体见这篇文章

计算属性获取

如果每次都通过 $store.state.状态名 来获取状态会显得比较繁琐,所以我们一般会使用 计算属性

// Vue2
export default {
  computed: {
    username() {
      return this.$store.state.username
    },
    password() {
      return this.$store.state.password
    }
  }
}
// Vue3
import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    // setup 函数中通过 useStore 方法获取 store 实例
    const store = useStore()
    
    const username = computed(() => store.state.username)
    const password = computed(() => store.state.password)

    return {
      username,
      password
    }
  }
}

这样在模板中我们直接使用计算属性就很方便

<template>
  <div>
    <div>{{ username }}</div>
    <div>{{ password }}</div>
  </div>
</template>

mapState 辅助函数

当一个组件需要获取多个状态值的时候,如果都手动一个个写成计算属性就会显得有点冗余。这时我们可以使用 mapState 辅助函数帮助我们生成计算属性。

mapState 函数返回一个对象。

mapState 函数根据传递过来的参数,将 返回对应状态的函数 返回。即:

mapState(['counter', 'username', 'password'])

// 相当于
return {
  counter: function() {
    return this.$store.state.counter
  },
  username: function() {
    return this.$store.state.username
  },
  password: function() {
    return this.$store.state.password
  }
}

这样我们就可以使用 mapState 函数配合计算属性:

import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['counter', 'username', 'password'])
  }
}

在模板中就可以直接使用了:

<template>
  <div>
    <div>{{ counter }}</div>
    <div>{{ username }}</div>
    <div>{{ password }}</div>
  </div>
</template>

由于 mapState 函数返回的是一个对象,所以如果没有其他计算属性,则可以简写成下面这种形式:

computed: mapState(['counter', 'username', 'password'])

mapState 函数也可以配合 methods 来使用:

import { mapState } from 'vuex'

export default {
  methods: {
    ...mapState(['counter', 'username', 'password'])
  }
}

但是我们一般不推荐这么用。

手动实现一个 mapState 函数

function mapState(state) {
  const obj = {}

  state.forEach((i) => {
    obj[i] = () => {
      return this.$store.state[i]
    }
  })

  return obj
}

setup 函数中使用 mapState 辅助函数

由于在 setup 函数中使用 computed 方法必须传入一个函数作为参数,所以我们不能像在 Vue2 中一样,直接把 mapState 函数的返回值赋值给 computed

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
  setup() {
    const store = useStore()
    
    const username = computed(() => store.state.username)
    const password = computed(() => store.state.password)

    return {
      username,
      password
    }
  }
}

所以我们只能通过另外的一种方式来使用 mapState 函数,代码如下:

import { computed } from 'vue'
import { useStore, mapState } from 'vuex'

export default {
  setup() {
    const store = useStore()
    
    // 我们的目的是把下面这种手动来写计算属性的方式变成自动生成对应的计算属性
    // const counter = computed(() => store.state.counter)
    // const username = computed(() => store.state.username)
    // const password = computed(() => store.state.password)

    const storeStateFn = mapState(['counter', 'username', 'password'])

    const storeState = {}
    Object.keys(storeStateFn).forEach((key) => {
      const fn = storeStateFn[key].bind({ $store: store })
      storeState[key] = computed(fn)
    })

    return {
      ...storeState
    }
  }
}

我们可以将上面的代码封装成一个 hook,在需要的地方直接调用:

import { computed } from 'vue'
import { useStore, mapState } from 'vuex'

export function useState(mapper) {
  const store = useStore()

  const storeStateFn = mapState(mapper)

  const storeState = {}

  Object.keys(storeStateFn).forEach((key) => {
    const fn = storeStateFn[key].bind({ $store: store })
    storeState[key] = computed(fn)
  })

  return storeState
}

在组件中调用:

import { useState } from '../hooks/useState'

export default {
  setup() {
    const storeState = useState(['counter', 'username', 'password'])

    return {
      ...storeState
    }
  }
}

Getters

state 中的属性需要进一步处理之后再使用,这个时候我们就可以使用 getters

getters 的基本使用

const store = createStore({
  state() {
    return {
      counter: 10
    }
  },
  getters: {
    showCounter(state) {
      return `当前计数:${state.counter}`
    }
  }
})

getters 类似于计算属性,可以在模板中直接使用:

<div>{{ $store.getters.showCounter }}</div>

image.png

getter 中可以调用其他的 getter

const store = createStore({
  state() {
    return {
      username: 'kobe',
      password: 123456
    }
  },
  getters: {
    showMessage(state, getters) {
      return `用户名:${state.username} ${getters.showPassword}`
    },
    showPassword(state) {
      return `密码:${state.password}`
    }
  }
})
<div>{{ $store.getters.showMessage }}</div>

image.png

getter 的参数

getter 一共有四个参数:state, getters, rootState, rootGetters,后面两个参数只有在模块化的时候才会用上。

getter 可以返回一个函数

在使用时可以直接调用这个函数。

const store = createStore({
  state() {
    return {
      counter: 10
    }
  },
  getters: {
    showCounterAddN(state) {
      return function (n) {
        return `当前计数:${state.counter + n}`
      }
    }
  }
})

因为 getters 不能传参,所以可以通过这种方式来传参。

<div>{{ $store.getters.showCounterAddN(12) }}</div>

image.png

mapGetters 辅助函数

使用方法同 mapState 函数一样,我们可以这样写:

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters(['showCounter', 'showName', 'showPassword'])
  }
}
<template>
  <div>
    <div>{{ showCounter }}</div>
    <div>{{ showName }}</div>
    <div>{{ showPassword }}</div>
  </div>
</template>

image.png

setup 函数中使用 mapState 函数

同样在 setup 中使用时我们也可以封装一个 hook

import { computed } from 'vue'
import { useStore, mapGetters } from 'vuex'

export function useGetters(mapper) {
  const store = useStore()

  const storeStateFn = mapGetters(mapper)

  const storeState = {}

  Object.keys(storeStateFn).forEach((key) => {
    const fn = storeStateFn[key].bind({ $store: store })
    storeState[key] = computed(fn)
  })

  return storeState
}

封装 useMapper

由于 useStateuseGetters 中的代码大部分是重复的,所以我们封装了 useMapper

import { computed } from 'vue'
import { useStore } from 'vuex'

export function useMapper(mapper, mapFn) {
  const store = useStore()

  const storeStateFn = mapFn(mapper)

  const storeState = {}

  Object.keys(storeStateFn).forEach((key) => {
    const fn = storeStateFn[key].bind({ $store: store })
    storeState[key] = computed(fn)
  })

  return storeState
}

useGetters

import { mapGetters } from 'vuex'
import { useMapper } from './useMapper'

export function useGetters(mapper) {
  return useMapper(mapper, mapGetters)
}

Mutations

基本使用

更改 Vuexstore 中的状态的唯一方法是提交 mutation

const store = createStore({
  state() {
    return {
      counter: 10
    }
  },
  mutations: {
    incrementN(state, payload) {
      console.log(payload) // 数据
      state.counter += payload.n
    },
    decrementN(state, payload) {
      state.counter -= payload.n
    }
  }
})

我们在提交 mutation 的时候,可以携带数据过去。

export default {
  methods: {
    addN(n) {
      // 第二个参数用来传递数据
      this.$store.commit('incrementN', { n, name: 'zs', age: 18 })
      
      // 也可以通过这种方式来提交 mutation
      this.$store.commit({ type: 'incrementN', n, name: 'zs', age: 18 })
    }
  }
}
<div>{{ $store.state.counter }}</div>
<button @click="addN(10)">+10</button>
<button @click="addN(20)">+20</button>

image.png

mapMutations 辅助函数

mapMutations 函数返回一个对象。

mapMutations 函数根据传递过来的参数,将 返回对应提交的函数 返回。即:

mapMutations(['incrementN', 'decrementN'])

// 相当于
return {
  incrementN: function(payload) {
    return this.$store.commit('incrementN', payload)
  },
  decrementN: function(payload) {
    return this.$store.commit('decrementN', payload)
  }
}

配合 methods 使用:

methods: {
  ...mapMutations(['incrementN'])
}

在模板中使用:

<div>
    <div>{{ $store.state.counter }}</div>
    <button @click="incrementN({ n: 10, name: 'zs', age: 18 })">+10</button>
    <button @click="incrementN({ n: 20, name: 'zs', age: 18 })">+20</button>
  </div>

setup 函数中使用 mapMutations 辅助函数

直接使用,然后导出就可以了:

import { mapMutations } from 'vuex'

export default {
  setup() {
    const storeMutations = mapMutations(['incrementN', 'decrementN'])

    return {
      ...storeMutations
    }
  }
}
<div>
    <div>{{ $store.state.counter }}</div>
    <button @click="incrementN({ n: 10, name: 'zs', age: 18 })">+10</button>
    <button @click="incrementN({ n: 20, name: 'zs', age: 18 })">+20</button>
  </div>

Actions

actions 类似于 mutations,区别在于, mutations 只允许进行同步的操作,而 actions 可以进行异步的操作。

action 必须通过提交 mutation 来修改 state,不能直接修改。

const store = createStore({
  state() {
    return {
      data: {}
    }
  },
  mutations: {
    getData(state, payload) {
      state.data = payload
    }
  },
  actions: {
    getDataAction(context, payload) {  // context 参数是当前上下文,相当于 store 实例
      axios.get('http://123.207.32.32:8000/home/multidata').then((res) => {       
        // 这里只能通过提交 mutation 来修改 state 中的状态,而不能直接进行修改
        context.commit('getData', res.data.data)
      })
    }
  }
})
// 分发一个 action,也可以传参
this.$store.dispatch('getDataAction', { name: 'zs', age: 18 })

actioncontext 参数是当前上下文,相当于 store 实例,我们可以从其中获取到 commit 方法来提交一个 mutation,或者通过 context.statecontext.getters 来 获取 stategetters

mapActions 辅助函数

mapActions 函数的使用类似于 mapMutations

setup 函数中使用 mapActions 辅助函数

类似于 mapMutations