vuex内容及使用详解

Vuex是什么

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

说起状态管理模式,就不得不提起一个名词——单向数据流。

单向数据流

在Vue中,数据的传输流动必须遵循单向数据流原则:父级 prop 的更新会向下流动到子组件中,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。

单向数据流有一个个人认为最容易理解的例子:props和( e m i t / emit/ emit/on)的组合。没错,就是和vuex 同为组件间通信方式之一的父子组件通信组合。

// 父组件
<child :info="info" @clickButton="handleClickButon"/>
export default {
  data(){
    return {
      info:{
        name: 'muyu',
        age: 28
      }
    }
  },
  methods:{
    handleClickButon (name) {
      this.info.name = name
    }
  }
}

// 子组件
<div>
  <p>{{info.name}}</p>
  <button @click="handleChangeName">修改</button>
</div>
export default {
  props:{
    info: {
      type: Object,
      default: () => { name: '' }
    }
  },
  methods: {
    handleChangeName () {
      this.$emit('clickButton','SkyRain')
    }
  }
}

父组件传递给子组件的info 数据,在子组件中显示。但子组件不能直接修改info 数据,必须通过派发自定义事件($emit)到父组件,在父组件中进行修改,修改后的值将会触发组件更新,子组件接收到的 info数据会刷新为最新的值

状态管理模式

单项数据流是一条必须遵守的原则,但是在某些情况下,原则性会遭到破坏

  • 多个组件依赖同一个数据状态
  • 不同的组件中操作需要修改同一个数据状态

在跨代组件通信和兄弟组件通信的情况下,props和( e m i t / emit/ emit/on)的组合并不适用,其他的组件通信方式(如:provide/inject、 a t t r s / attrs/ attrs/listeners、ref/$refs等)并不能同时满足,所以需要有一个新的状态管理模式来实现对多种情况下的数据通信进行管理。

全局单例模式

全局的概念,在JS中并不难理解。

JS中作用域分为全局作用域、函数作用域、块级作用域。

在浏览器中,全局对象是window;而在node服务端中,全局对象是global。

而单例模式,则是来自于软件开发中的设计模式,即:一个类仅有一个实例,且提供一个访问的全局访问点。

我个人的理解则是:脱离当前对象的所属范围,归属于最高范围且只有一个,遵循一定的规则下可以进行内容的读取和修改

基本思想

以全局单例模式管理,通过定义和隔离状态管理中的各种概念,并通过强制规则维持视图和状态间的独立性,使代码变得更结构化且易维护。

Vuex插件注册

由于是全局单例模式,在模块化的系统中,如果组件中使用vuex就需要频繁的的引入,相当麻烦。

通过Vue的插件系统进行注册,将vuex实例从根组件注入到所有子组件中,子组件中通过this.$store.xxx可以访问到内部变量

import Vue from 'vue'
import Store from 'store/index.js'

new Vue({
  el: '#app',
  xxx,
  store,
  render: h => h(App)
})

Vuex核心概念

核心概念或者说是核心使用点,共有5个:State、Getter、Mutation、Action、Module

State

在上文中提到的对于全局单例模式的我的理解:脱离当前对象的所属范围,归属于最高范围且只有一个,遵循一定的规则下可以进行内容的读取和修改。

其中 ‘归属于最高范围且只有一个’这一段话,就是指用一个对象包含了当前应用的所有状态,即每个应用只存在一个实例,在Vuex中被称为单一状态树

State中存储的数据和组件中data存储数据遵循相同的规则。

  • state数据定义
const store = new Vuex.Store({
  state: {
    userInfo: {},
    pageTitle: '',
    xxx
  }
})

  • state数据使用

state数据的使用,个人感觉基本没什么约束。但是一般会把state中数据自页面中通过计算属性代替使用,个人猜测原因可能有两种:

  • 本着程序员的美好品德(写最少的代码,做最多的事)
  • 响应式数据变化时候可使计算属性变化,从而触发DOM更新(来源于官方文档)
// 直接使用,适用于不怎么修改的变量
let objProp = this.$store.state.xxx
// 计算属性中访问实例
export default{
  computed: {
    userInfo () {
      return this.$store.state.userInfo
    }
  }
}
// 辅助函数使用(mapState返回的是一个对象)
import {mapState} from ‘vuex’,
export default{
  // 计算属性仅有state数据
  computed: mapState({
    userInfo: state => state.userInfo
  })
  // 计算属性有其他局部属性
  computed: {
    companyName(){ xxx },
    ...mapState({
      userInfo: state => state..userInfo
    })
  }
}

  • state辅助函数

辅助函数的作用其实就是为了方便使用,不惜要去重复书写 this.$store

Getter

Getter属性,官方的解释是state中数据的派生状态,可以认为是vuex实例中的计算属性。

  • Getter数据定义
const store = new Vuex.Store({
  state: {
    userInfo: {},
    pageTitle: '',
    xxx
  },
  getters:{
   // 一个参数为state
    userToken (state) {
      return state.userInfo.token
    },
    // 两个参数为state、其他getters
    userToken (state,otherGetters) {
      return otherGetters.username
    }
  }
})
  • Getters数据使用
// 直接使用
let objProp = this.$store.getters.xxx
// 计算属性中访问实例
export default{
  computed: {
    userInfo () {
      return this.$store.getters.userToken
    }
  }
}
// 辅助函数使用(mapState返回的是一个对象)
import {mapGetters } from ‘vuex’,
export default{
  // 计算属性仅有state数据
  computed: mapGetters ({
    'userToken',
    xxx
  })
  // 计算属性有其他局部属性
  computed: {
    companyName(){ xxx },
    ...mapGetters ({
      'userToken',
      xxx,
      // 取另外的名字
      'personToken': 'userToken'
    })
  }
}
Mutation

Mutation属性,很类似于组件内的事件,也是更改Vuex中state数据的唯一方法:store.commit(xxx)

mutation必须是同步函数,其中不能有异步回调。

  • mutation数据定义
const store = new Vuex.Store({
  mutations: {
    increment (state, payload) {
      state.count += payload
    },
    incrementObj (state, payload) {
      state.count += payload.count
    }
  }
})
  • commit调用
const store = new Vuex.Store({
  mutations: {
    increment (state, payload) {
      state.count += payload
    },
    incrementObj (state, payload) {
      state.count += payload.count
    }
  }
})
// 直接提交commit方法
store.commit('increment')
// 提交载荷:commit方法的第二个参数(可以是具体值,也可以是对象)
store.commit('increment',10)
store.commit('incrementObj ',{count:10})
// 对象风格提交
store.commit({
  type: 'incrementObj ',
  count: 10
})

  • 使用常量代替事件类型

这种模式的使取决于自己项目情况,从我个人的经历来看:管理后台中对于vuex的使用较少,可以不用;移动端项目可能用的比较多,看情况使用。

// mutationType.js
export const GET_USERINFO = 'GET_USERINFO'

// store.js
import {GET_USERINFO} from './mutationType.js'
const store = new Vuex.Store({
  mutations: {
    [GET_USERINFO](state, payload) {
      state.userInfo = payload
    }
  }
})

  • 辅助函数

辅助函数mapMutation会把组件中的methods映射为store.commit调用

import { mapMutations } from 'vuex'
export default {
  methods: {
    // 数组形式 
    ...mapMutations([
      'GET_USERINFO',
      xxx
    ]),
    // 对象换名形式
    ...mapMutations({
     getUserInfo: 'GET_USERINFO'
    })
  }
}
Action

Action与Mutation有相同点也有不同点:

相同点:二者都类似于组件内的methods方法

不同点:

  • Action里可以进行任意的异步操作

  • Action内不能直接变更state数据,可以通过提交commit('xxx) mutations方法进行修改

  • action数据定义

const store = new Vuex.Store({
  actions: {
    increment (context) {
      context.commit('xxx)
    },
    incrementObj ({commit,state,getters}) {
      commit('xxx)
    }
  }
})

context对象和store实例具有相同方法和属性,但是并不等于store实例。

  • dispatch方法

actions中有一个与mutations中commit用法相似的方法:diapatch,意为分发

// 直接使用
this.$store.dispatch('xxx')
// 载荷形式
this.$store.dispatch('xxx'{count: 10})
this.$store.dispatch({
  type: 'xxx',
  count: 10
})
// 辅助函数mapActions
import { mapActions } from 'vuex'
export default {
  methods: {
    // 数组形式 
    ...mapActions ([
      'getUserInfo',
      xxx
    ]),
    // 对象换名形式
    ...mapActions ({
     getInfo: 'getUserInfo'
    })
  }
}

Module

使用单一状态树,store实例中存储着所有的状态,复杂情况下,store实例会很臃肿,寻找某一个变量相关会很困难,所以module支持按照模块进行划分,甚至子模块嵌套

  • moules数据定义
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})
  • 根节点状态

主要是针对于模块内部的action,根节点的state以rootState 存在与子模块的context中

const moduleA = {
  // ...
  actions: {
    getUserInfo ({ state, commit, rootState }) {
      if (state.name && rootState.token) {
        commit('userInfo')
      }
    }
  }
}

对于模块内部的getter,根节点状态作为第三个参数暴露出来

const moduleA = {
  // ...
  getters: {
    userToken (state, getters, rootState) {
      return state.token || rootState.token
    }
  }
}
  • 命名空间

默认情况下,虽然按照模块进行了划分,但是都是注册在全局空间内的。就会造成一个问题:如果出现了重名,就可能会导致错误。

想要避免这种情况,使模块具有更高的封装度和复用性,可以添加 namespaced: true来是模块分隔到具体的命名空间

当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

const store = new Vuex.Store({
  modules: {
    user: {
      namespaced: true,
      state: () => ({...})
      ...
      modules: {
        app: {
          state: () => ({...})
          ...
        },
        // 嵌套命名空间
        permission: {
          namespaced: true,
          state: () => ({...})
          ...
        }
      }
    }
  }
})
  • 命名空间内访问根节点内容

这里和之前的根节点部分不同的地方在于:

  • 之前的根节点内容访问仅在action内容中
  • 此处可以getter中访问根节点状态和根节点的getter;可以在action/mutation中分发或提交根节点内容,加上第三参数{root:true}即可
const store = new Vuex.Store({
  getters: {
    someGetter () {}
  },
  mustations: {
    someMutation () {}
  },
  actions: {
    someAction () {}
  },
  modules: {
    sys: {
      namespaced: true,
      getters: {
        someGetter () {}
      },
      mustations: {
        someMutation () {}
      },
      actions: {
        someAction () {}
      },
    },
    user: {
      namespaced: true,
      getters: {
        someGetter (state,getters,rootState,rootGetters) {
          getters.someGetter // user/somGetter
          rootGetters.someGetter // someGetter
          rootGetters['sys/someGetter'] // sys/someGetter
        }
      },
      mustations: {
        someMutation () {}
      },
      actions: {
        someGlobalAction () {
          root: true, // 在命名空间模块中注册全局action
          handler (namespacedContext, payload) {}
        },
        somAction ({dispatch,commit,getters,rootGetters}) {
          getters.someGetter // -> 'foo/someGetter'
          rootGetters.someGetter // -> 'someGetter'
          rootGetters['sys/someGetter'] // -> 'sys/someGetter'
          
          dispatch('someAction') // -> 'user/someAction'
          dispatch('someAction', null, { root: true }) // -> 'someAction'
  
          commit('someMutation') // -> 'user/someMutation'
          commit('someMutation', null, { root: true }) // -> 'someMutation
        }
      }
    }
  }
})
  • 命名空间模块辅助函数使用简化

有大量命名空间模块的情况下使用辅助函数会比较麻烦,可以进行简化

// 简化前
computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  }),
  ...mapGetters([
    'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
    'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
  ])
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}
// 简化后
computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  }),
  ...mapGetters('some/nested/module', [
    'someGetter', // -> this.someGetter
    'someOtherGetter', // -> this.someOtherGetter
  ])
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}
// createNameSpacedHelpers 函数
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}