Vuex

197 阅读5分钟

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

安装Vuex

1、通过脚手架创建项目
通过脚手架创建项目可以勾选Vuex选项,会根据项目使用的是 vue2 还是 vue3 自动安装对应版本的 Vuex。

2、在已有项目上手动安装
在已有项目上通过 npm i 手动安装Vuex,需根据项目使用 vue2 还是 vue3 选择 Vuex 的版本,如果使用的是 vue2 ,则须安装 Vuex3,对应命令 npm i vuex@3 ,如果使用的是 vue3 ,则须安装 Vuex4,对应命令 npm i vuex@4 。

手动安装vuex后须在 src 文件夹下新添 store 文件夹,在 store 文件夹下创建 index.js 文件。

vuex工作原理图

)83C9SZT832M6D{GLO939%L.png

配置vuex

// index.js 改文件用于创建vuex中最为核心的store

// 引入vue
import Vue from 'vue'
// 引入vuex
import Vuex from 'vuex'
// 把vuex 使用use方法 引入vue中
Vue.use(Vuex)

// 可以使用modules把vuex模块化
const modules = {

}
// 准备state--用于存储数据
const state = {
    msg:'state初始值,组件共享的数据',
    num:0
}
// 准备actions--用于响应组件中的动作
// actions是用来调后台接口的并commit提交一个mutation
const actions = {

}
// 准备mutations--用于操作数据
// 在mutations中修改state中的值(修改state中的值想要在devtools中留下记录必须使用mutations修改)
const mutations = {
    ADDNUM:function(state){
        state.num++
        console.log('ADDNUM',state);
    },
    DELNUM:function(state){
        state.num--
        console.log('DELNUM',state);
    }
}
// 计算组件中的数据,可以对数据进行二次加工,类似computed功能
const getters = {

}

// 导出一个Vuex.Store的实例化对象
export default new Vuex.Store({
    state,
    actions,
    mutations,
    getters,
    modules
})
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
// 引入Vuex.Store的实例化对象store
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  // 全局传入store配置项
  router,
  render: h => h(App)
}).$mount('#app')

State

state 用于存储所有组件都可以使用的公共数据,当state中的数据发生改变,所有使用这些数据的组件视图也会随之重新渲染。

使用state中的数据

所有组件都可以通过this.$store.state获取state中的数据。

<h1>{{$store.state.msg}}</h1>
<h1>{{$store.state.str}}</h1>
<h1>{{$store.state.num}}</h1>

使用计算属性能让我们更简单地使用state中的数据。

<template>
    <h1>{{msg}}</h1>
</template>

<script>
export default {
    name:'App',
    computed:{
        msg(){
            return this.$store.state.msg
        }
    }
}
</script>

使用 mapState 辅助函数生成计算属性

当需要使用state中的多个数据时,无论是使用$store.state直接插入插值表达式,亦或是为每个数据声明一个计算属性,代码都会显得重复与冗余,我们可以使用 mapState 辅助函数帮助我们生成计算属性。

import {mapState} from 'vuex'

export default {
    name:'App',
    computed:{
        // msg(){
        //     return this.$store.state.msg
        // }
        
        // 当映射的计算属性的名称与 state 的子节点名称相同时,我们可以给 `mapState` 传一个字符串数组。
        ...mapState(['msg','str','num'])
        
        // 当映射的计算属性的名称与 state 的子节点名称不同时,我们需要给 `mapState` 传一个对象。
        // getMsg(){
        //     return this.$store.state.msg
        // }
        ...mapState({getMsg:'msg',getStr:'str',getNum:'num'})
    }
}

Getters

有时我们需要对state中数据进行筛选再使用,如果有多个组件需要用到这个筛选的数据,我们可以在getters中定义这个数据。 getters可以认为是store的计算属性,在Vue 2中,getters的结果像计算属性一样会被缓存起来,直到state中对应的数据发生改变。

// Getter 接受 state 作为其第一个参数
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: (state) => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

我们可以通过this.$store.getters,以属性的形式获取getters的结果。在使用时可以通过插值表达式直接引入,也可以声明一个计算属性。

<h1>{{$store.getters.doneTodos}}</h1>

computed: {
  doneTodos () {
    return this.$store.getters.doneTodos
  }
}

使用 mapGetters 辅助函数生成计算属性
同样的,我们可以使用 mapGetters 辅助函数帮助我们生成计算属性。

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果你想将一个 getter 属性另取一个名字,使用对象形式:

...mapGetters({
  //`this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutations

想要更改 state 中的数据,我们必须通过commit提交一个mutation,每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler),回调函数接受 state作为第一个参数,也可以传入额外的参数作为第二个参数。

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    // 每个mutation的回调函数名一般大写,用以和actions里的回调函数名作区分
    INCREMENT (state) {
      // 变更状态
      state.count++
    }
  }
})

// 组件内的方法。通过一个事件调用`commit`方法去调用 `mutation` 处理函数
methods:{
    add(){
        this.$store.commit('INCREMENT')
    }
}
// ...
mutations: {
  INCREMENT (state, n) {
    state.count += n
  }
}

// 组件内的方法。通过一个事件调用`commit`方法去调用 `mutation` 处理函数
methods:{
    add(){
        this.$store.commit('INCREMENT',2)
    }
}

我们不能直接调用一个 mutation 处理函数,必须通过调用 store.commit 方法,去调用一个 mutation 处理函数。如果不用通过后台接口传入额外的参数,我们可以通过一个事件调用commit方法去调用 mutation 处理函数。如果需要通过后台接口,则先需要dispatch调用一个action(关于action见下Actions),然后在action的回调函数里调用commit方法去调用 mutation 处理函数。

// 不需要调用后台接口
methods:{
    add(){
        this.$store.commit('INCREMENT')
    }
}
// 需要调用后台接口
methods:{
    add(){
        this.$store.dispatch('increment')
    }
}

const store = createStore({
  state: {
    count: 1
  },
  actions:{
    increment(context){
        context.commit('INCREMENT')
      }
  },
  mutations: {
    INCREMENT (state) {
      // 变更状态
      state.count++
    }
  }
})

使用mapMutations辅助函数

使用mapMutations辅助函数可以帮我们省略在methods中去定义一个事件调用commit方法。

methods:{
    // 数组写法
    ...mapMutations(['ADDNUM','DELNUM'])
    
    // 对象写法
    ...mapMutations({add:NUM',del:'DELNUM'})
}
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num:0
  },
  getters: {
  },
  mutations: {
    ADDNUM(state,value){
      state.num += value
    },
    DELNUM(state,value){
      state.num -= value
    }
  }
})

Actions

因为 mutation 必须同步执行这个限制,所以我们在actions内部执行异步操作:

actions: {
    addnum({commit},value){
      setTimeout(() => {
        commit('ADDNUM',value)
      }, 1000);
    },
    delnum({commit},value){
      setTimeout(() => {
        commit('DELNUM',value)
      }, 1000);
    }
  }

Action 通过 dispatch 方法触发:

export default {
    methods:{
       add(){
           this.$store.dispatch('addnum',2)
       }
    }
}

使用mapActions函数

同样的,使用mapActions辅助函数可以帮我们省略在methods中去定义一个事件调用commit方法。

import {mapActions} from 'vuex'

export default {
    methods:{
        // 数组写法
        ...mapActions(['addnum','delnum']), 
        
        // 对象写法
        ...mapActions({add:'addnum',del:'delnum'})
    }
}

关于通过commit提交mutation的的注意点

vuedevtools工具会同步记录mutation触发时state的状态。如果mutation是异步函数,或者在组件事件中异步调用调用dispatchcommit方法,都可能会造成devtools中记录的数据混乱。

1、mutation必须是同步函数

如果mutatiopn是异步函数,当mutation触发时回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    INCREMENT (state) {
      setTimeout(()=>{
          state.count++
      },500)
      
    }
  }
})

2、在组件的事件中必须同步调用dispatch或commit

在组件中异步调用dispatchcommit方法同样会造成devtools中记录的数据混乱。

下图在一秒内触发了三次 increment 回调函数 异步,一秒后调用 dispatch 方法的回调函数才触发继而调用commit方法触发mutation,此时this.num的值为6,因此devtools的三条记录中state.num分别是6、12、18。 bbcdc308130baef28dc2788abb2d09b.jpg

Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

// modeA.js
const state = ()=>({
    modeAMsg:'模块A的msg'
})

const getters = {
    getMsgOfA:(state)=> state.modeAMsg+'--getters'
}

const mutations = {
    CHANGEMSGOFA(state,value){
        state.modeAMsg = value
    }
}

const actions = {
    changemsgAwait({commit},value){
        setTimeout(() => {
            commit('CHANGEMSGOFA',value)
        }, 3000);
    }
}

export default {
    // 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 `namespaced: true` 的方式使其成为带命名空间的模块。
    namespaced:true,
    state,
    getters,
    mutations,
    actions
}
// index.js
import Vue from 'vue'
import Vuex from 'vuex'
import modeA from '@/store/modules/modeA'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    modeA
  }
})

在组件中使用modeA
通过this.$store.state.modeA.modeAMsg可以访问modeA中的modeAMsg
通过this.$store.getters['modeA/getMsgOfA']可以访问modeA中的getMsgOfA
调用commit方法调用modeA中的mutation

changeFn1(){
      this.$store.commit('modeA/CHANGEMSGOFA',prompt('请输入改变后的值'))
    },

调用dispatch方法调用modeA中的action

changeFn2(){
      this.$store.dispatch('modeA/changemsgAwait',prompt('请输入改变后的值'))
    },

使用辅助函数

当使用 mapStatemapGettersmapActions 和 mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐,可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。

import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'

export default {
  computed:{
    ...mapState('modeA',['modeAMsg']),
    ...mapGetters('modeA',['getMsgOfA'])
  },
  methods:{
    ...mapMutations('modeA',['CHANGEMSGOFA']),
    ...mapActions('modeA',['changemsgAwait'])
  }
}

一个结合以上两种用法的小demo

<template>
  <div id="app">
    <h1>App</h1>
    <h2>Modules</h2>
    <h3>模块A的msg:{{$store.state.modeA.modeAMsg}}</h3>
    <h3>模块A的msg(mapState):{{modeAMsg}}</h3>
    <h3>模块A的getMsgOfA:{{$store.getters['modeA/getMsgOfA']}}</h3>
    <h3>模块A的getMsgOfA(mapGetters):{{getMsgOfA}}</h3>
    <P>
      <button @click="changeFn1">改变模块A的msg</button> |
      <button @click="changeFn2">异步改变模块A的msg</button> |
      <button @click="changeFn3">改变模块A的msg(mapMutations)</button> |
      <button @click="changeFn4">异步改变模块A的msg(mapActions)</button>
    </P>
  </div>
</template>

<script>
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'

export default {
  name: 'App',
  data(){
    return {
      count:0
    }
  },
  created(){
    console.log(this);
  },
  computed:{
    ...mapState(['num']),
    ...mapState('modeA',['modeAMsg']),
    ...mapGetters('modeA',['getMsgOfA'])
  },
  methods:{
    ...mapMutations(['ADDNUM','DELNUM']),
    ...mapActions(['addnum','delnum']),
    ...mapMutations('modeA',['CHANGEMSGOFA']),
    ...mapActions('modeA',['changemsgAwait']),
    changeFn1(){
      this.$store.commit('modeA/CHANGEMSGOFA',prompt('请输入改变后的值'))
    },
    changeFn2(){
      this.$store.dispatch('modeA/changemsgAwait',prompt('请输入改变后的值'))
    },
    changeFn3(){
      this.CHANGEMSGOFA(prompt('请输入改变后的值'))
    },
    changeFn4(){
      this.changemsgAwait(prompt('请输入改变后的值'))
    },
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>