一文带你搞懂vue中全局状态管理vuex

4,839 阅读3分钟

为什么我们需要vuex

首先vue的官方对于vuex给出的定义是:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式

状态管理模式,理解起来可能还有点抽象,说通俗点就是解决vue框架中各个组件数据共享的一种方式,有点类似于全局的数据仓库,在vuex中维护的state对象,可以流程到各个vue实例上。那么它是为什了什么场景而生的呢?

了解vue框架的话,都知道vue2.0是通过选项的方式传入一个option生成vue实例,每个option中要定义一个data属性,或者计算属性,这份内部维护的数据可以进行数据于视图的绑定,也就是mvvm双向绑定。

如果是比较简单的应用的话,多个vue组件之间没有设计的数据共享的情况下,vue内部维护的一份数据还可以可以解决的,如果设计的多个组件的数据共享,那么很显然这种场景并不能胜任,虽然vue框架内部也提供了几种组件通信的技术,如通过props/emit进行父子组件的通信的方式,但是依然不能胜任复杂的需要多个组件共享数据场景。所以vuex就应允而生了。

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state) 。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

vuex由哪几个部分组成

state

上面已经提到vuex的本质其实是维护了一份能在应用内共享的全局数据。这就需要包含一个数据容器,在vuex里对应的是state对象,通过键值的形式映射了一系列需要全局共享的数据。

举一个例子,加入我们想要实现一个计数器应用,希望他的count能在全局共享。可以先定义一个state容器。 引入vuex,并实例化Store实例。

const state = {
    todos: [
      { text: '买菜', done: true },
      { text: '做饭', done: false }
    ]
  },

现在,在任何的vue组件中都可以通过 store.state 来获取状态对象。

getter

vuex允许我们对 store 中的 state 中派生出一些状态,这样的状态可以放在getters对象下。可以认为是 store 的计算属性,就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

例如有一个场景,对列表进行过滤并计数:如果不借助getters属性的话,他是这样的

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

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

为了代码的简洁行性,这部分的逻辑可以放在getters里,Getter 接受 state 作为其第一个参数,Getter 也可以接受其他 getter 作为第二个参数。

const getters = {
    count: 0,
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
        return getters.doneTodos.length
    }
}

现在可以在项目中通过store.getters.doneTodos访问到这些变量

mutation

有了数据容易后,vuex对于数据的维护即对数据的改变的有一套自己的规定,vue官方强调,我们通过提交 mutation 的方式,而非直接改变 store.state.todo,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。

定义 mutations 的方式也非常简单,只要将state对应属性的改变封装成方法放在mutations对象里面即可

const mutations = {
    setCount (state, value) {
        state.count = value
    },
    addTodo(state, item){
        state.todo.push({
            text: item,
            done: false
        })
    }
}

需要注意的是mutation 必须是同步函数 如果mutation函数中包含了异步的逻辑,如下面所示

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用。

分发 Mutation

定义完mutations后,可以通过 store.commit 方法触发状态变更。

store.commit('setCount', 10)

同时也支持另一种通过对象风格的提交方式

store.commit({
  type: 'increment',
  amount: 10
})

action

刚刚我们提到mutation当中不能包含异步方法导致状态改变的改变的逻辑,那样会使得程序的状态难以调试。所以在vuex中需要将这两种方式导致的状态改变区分开,所以vuex提供了另一个对象用于异步状态的改变--action。 action 类似于 mutation,不同在于:

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,这个context 对象里包含了store实例里相同的属性,如state,getters, 也可以通过commit方法发起一个mutation调用。

const actions = {
    setCount (context, value) {
        context.commit('setCount', value)
    }
}

实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):

const actions = {
    setCount ({ commit }, value) {
        commit('setCount', value)
    }
}

除了可以在action内部提交一个commit外,action中还可以执行异步逻辑

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

Actions 支持同样的对象方式进行分发:

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

如何使用vuex

使用vuex之间,需要先引入它,通过npm的方式下载

npm install vuex --save

下载完vuex后需要在vue应用程序内全局注册这个组件,即显式地通过 Vue.use() 来安装 Vuex:
index.js

import Vue from 'vue'
import Vuex from 'vuex'
import { state, getters, mutations, actions } from './state'

Vue.use(Vuex)
const store = new Vuex.Store({
    state,
    getters,
    mutations,
    actions,
})

这样就为这个整个vue应用维护了一份全局的数据状态管理。里面定义了state全局的数据状态,同时也定义了一系列的mutations和actions对这些状态更新进行提供同步和异步的更新方式。

辅助函数

vuex中提供了几个辅助函数用以更加方便的在组件中使用这份全局状态数据。

mapState

当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性:

import { mapState } from 'vuex'

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

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // state状态和组件本身的数据派生出一个新的计算属性
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。这也是项目中常见的使用场景

computed: mapState([
  // 映射 this.count 为 store.state.count
  'count'
])

值得一提的是mapState 函数返回的是一个对象。 在有需要将mapState结合组件本身的局部数据一起使用时可以使用对象展开运算符的方式

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

mapGetters

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

mport { mapGetters } from 'vuex'

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

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

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

mapMutations

除了上面提到的可以使用 this.$store.commit('xxx') 提交 mutation以外,vuex中对mutations的方法也提供了 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用。

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')`
    })
  }
}

mapAction

同样的vuex也提供了mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用。

import { mapActions } from 'vuex'

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

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

module

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

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter。

onst 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 的状态