Vuex

概念

Vuex 是一个专为 Vue.js 应用程序开发的

状态管理模式
。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能

工作流程

Vuex有5个重要的属性:

  1. state 存放状态
  2. mutations 成员操作
  3. getters 加工state成员给外界
  4. actions 异步操作
  5. modules 模块化状态管理

vuex1

如图所示,首先 Vue 组件如果调用某个 VueX 的方法过程中需要向后端请求时或者说出现异步操作时,需要 dispatch VueXactions 的方法,以保证数据的同步。可以说,action 的存在就是为了让 mutations 中的方法能在异步操作中起作用。

如果 actions 方法中没有任何操作,可以直接在组件内使用commit 来调用 mutations 中的方法。

使用Vuex(简单案例)

在使用之前首先要安装 Vuex,需要注意的是,本文案例使用的是vue2,目前npm install vuex 默认安装的是Vuex4,Vuex4在vue2中不能用。所以我们需要安装 Veux3

npm i vuex@3

创建一个store文件夹,文件夹下创建一个 index.js

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

//准备state——用于存储数据
const state = {
	count: 0
},
const mutations = {
  increment (state) {
      state.count++
  }
}
//创建并暴露store
export default new Vuex.Store({
	mutations,
	state,
})

注意

  1. Vue.use(Vuex) 使用Vuex插件
  2. new Vuex.Store 创建一个store

然后再 main.js 中引入 index.js,并注入到 Vue实例 中。

import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false

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

此时在组件模板中可以使用 $store.state 来获取状态对象,以及通过 $store.commit 方法触发状态变更:

<template>
	<div>
		<h1>{{$store.state.count}}</h1>
	</div>
</template>
computed: {
  increment() {
    this.$store.commit('increment')
    console.log(this.$store.state.count)
  }
}

注意

不能直接改变 store 中的状态(虽然能改),改变 store 中的状态的唯一途径就是显示地 提交(commit) mutation。这样使得我们可以方便追踪每一个状态的变化。

接下来我们具体来说一下 Vuex 中的各种属性

State(单一状态树)

state 就是Vuex中的公共的状态,和组件中的 data 类似,用于保存所有组件的公共数据。

//main.js
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

Vue.use( Vuex )

const store = new Vuex.Store({
  state:{ 
    products: [
      {name: '鼠标', price: 20},
      {name: '键盘', price: 40},
      {name: '耳机', price: 60},
      {name: '显示屏', price: 80}
    ]
  }
})

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

mapState 辅助函数

当一个组件需要获取多个状态的时候,将这些状态都声明计算属性会有些重复和冗余。

computed: {
  count() {
    this.$store.state.count
  },
  count2() {
    this.$store.state.count2
  },
  count2() {
    this.$store.state.count2
  }
}

为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。首先要引用 mapstate

import { mapState } from 'vuex'

export default {

  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count2 + this.localCount
    }
  })
}

当计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

computed: mapState(['count','count2'])

// 等同于
computed: mapState({
  count: state => state.count
  count2: state => state.count2
}}

计算属性(computed)是一个对象,mapState 返回的也是一个对象,上面案例我们将mapState作为计算属性的返回值,此时计算属性中不能再写其他方法。我们如何将它与局部计算属性混合使用呢?

此时就要使用到 es6 中的,对象展开运算符了。

computed:{
  // 相当于将mapState返回值对象中的属性,拿出来放到 computed对象中
  ...mapState({
      count: state => state.count
      count2: state => state.count2
  }},

  //其他方法
  computed1(){
    return "hello"
  }
}

Getter

getter 和组件中的计算属性(computed)类似,有时候我们需要从 store 中派生出一些状态,例如对列表进行过滤并计数:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义 getter(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 可以接受两个参数。

  1. 第一个参数为 state
  2. 接受其他 getter 作为第二个参数,将getters下的其他getter拿来用
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state,getters) => {
     return getters.doneTodos.length
    }
  }
})

我们可以很容易地在任何组件中使用它:

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

mapGetters辅助函数

和mapState一样。如果获取多个 getter 都声明计算属性会有些重复和冗余。此时就可以使用mapState

import {mapGetters} from 'vuex'

export default {

  computed:{
    ...mapGetters(['doneTodos','doneTodosCount'])

    // 等价于
    ...mapGetters({
      doneTodos:(state) => this.$store.getters.doneTodos,

      doneTodosCount:(state,getters) => this.$store.getters.doneTodosCount
    })
  }
}

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

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

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。会接受 state 作为第一个参数

在开发中我们一般使用常量作为事件类型。

const store = new Vuex.Store({
    state:{
        counter: 1000
    },
    mutations: {
        INCERMRNT(state){
            state.counter++
        }
    },
})

在组件中使用 $store.commit 调用 INCERMRNT

this.$store.commit('INCERMRNT')

提交载荷(Payload)

就是在使用this.$store.commit同时传入额外的参数。即 mutation 的 载荷(payload)

const store = new Vuex.Store({
    state:{
        counter: 1000
    },
    mutations: {
        INCERMRNT(state,n){
            state.counter += n
        }
    },
})
methods: {
    add(){
      this.$store.commit('INCERMRNT',5)
    }
}

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

// ...
mutations: {
  INCERMRNT (state, payload) {
    state.count += payload.amount
  }
}
this.$store.commit('INCERMRNT', {
  amount: 10
})

mapMutations

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

Mutation 需遵守 Vue 的响应规则

既然 Vuexstore 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  1. 最好提前在 store 中初始化好所有所需属性。
  2. 当需要在对象上添加新属性是,你应该
    • 使用 Vue.set(obj,'newProp',123)
     import Vue from 'vue'
     const store = new Vuex.Store({
       state: { 
         info: {
           name: 'zxq',
           age: 23
         }
       },
       mutations: {
         UPDATE_INFO (state) {
           Vue.set(state.info,'address','山东')
         }
       }
     })
    

此时调用UPDATE_INFO就会给info添加一个新属性address 3. mutation 必须是同步函数

Actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

让我们来注册一个简单的 action:

import Vue from 'vue'
const store = new Vuex.Store({
  state: { 
  	info: {
      name: 'zxq',
      age: 23
    }
  },
  mutations: {
    updateInfo (state) {
       Vue.set(state.info,'address','山东')
    }
  },
  actions:{
      //context:上下文
      aUpadteInfo(context) {
        setTimeout(() => {
           context.commit('updateInfo')
         })
       }
    }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 stategetters

通过 this.$store.dispatch() 在组件中触发 action

updateInfo(){
   this.$store.dispatch('aUpadteInfo')
}

开发中,我们经常使用 参数解构({commit}) 来简化代码(特别是我们需要调用 commit 很多次的时候)

 actions:{
  //context:上下文
  aUpadteInfo({ commit }) {
    setTimeout(() => {
        commit('updateInfo')
      })
    }
}

Actions 支持与mutation同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
this.$store.dispatch('aUpadteInfo', {
  amount: 10
})

// 以对象形式分发
this.$store.dispatch({
  type: 'aUpadteInfo',
  amount: 10
})

Module

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

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

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

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

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在

全局命名空间
的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。

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

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      }
    }
  }
})

加上 namespaced: true 后,调用 action 的路径为 dispatch('account/login') 其他如上代码所示。

启用了命名空间的 getter 和 action 会收到局部化的 getter,dispatch 和 commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

在带命名空间的模块访问全局内容

如果你希望使用全局 state 和 getter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要调用全局的 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

在带命名空间的模块注册全局 action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

带名称空间的绑定函数知识点

当使用 mapState, mapGetters, mapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

export default new Vuex.Store({
	modules:{
		countAbout:countOptions,
		personAbout:personOptions
	}
})
// countOptions
export default {
	namespaced:true,
	actions:{},
	mutations:{},
	state:{
		sum:0, //当前的和
		school:'尚硅谷',
		subject:'前端',
	}
}
computed: {
  ...mapState({
    sum: state => state.countAbout.sum
  })
},
methods: {
  ...mapActions([
    'countAbout/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

computed: {
  ...mapState('countAbout', {
    sum: state => state.sum,
    school: state => state.school
  })

  // 或者
  ...mapState('countAbout', ["sum","school"])

  // 或者,表示取出countAbout,具体取值如:countAbout.sum
  ...mapState(["countAbout"])
},
methods: {
  ...mapActions('countAbout', [
    'countAbout', // -> this.countAbout()
    'bar' // -> this.bar()
  ])
}