理解 Vuex 的核心概念

在 Vuex 中有五个核心概念,我们将在本节中引导你了解它们。它们是 state、getters、mutations、actions 和 modules。我们将在下一节首先介绍 state 这个概念。

State

State 是 Vuex store 的核心。它是我们可以在 Vuex 中以结构化和可预测的方式管理和维护的 “全局” 数据的来源。Vuex 中的 state 是一个单一的状态树——一个包含应用程序所有状态数据的 JavaScript 对象。因此,每个应用程序通常只有一个 store。让我们在接下来的章节中看看如何在组件中获取 state。

访问 State

正如我们在上一节中提到的,Vuex store 是响应式的,但是如果想在视图中访问响应式的值,我们应该使用计算属性(computed property)而不是 data 方法,如下所示:

// vuex-sfc/state/basic/src/app.vue
<template>
  <p>{{ number }}</p>
</template>

<script>
import Vue from 'vue/dist/vue.js'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: { number: 1 }
})

export default {
  computed: {
    number () {
      return store.state.number
    }
  }
}
</script>

现在,<template> 块中的 number 字段是响应式的,每当 store.state.number 改变时,计算属性都会重新计算并更新 DOM。但是,这种模式会导致耦合问题,并且与 Vuex 的提取思想相悖。因此,让我们按照以下步骤重构前面的代码:

  1. 将 store 提取到根组件中:

    // vuex-sfc/state/inject/src/entry.js
    import Vue from 'vue/dist/vue.js'
    import App from './app.vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: { number: 0 }
    })
    
    new Vue({
      el: 'app',
      template: '<App/>',
      store,
      components: {
        App
      }
    })
  2. 从子组件中移除 store,但保留计算(computed)属性不变:

    // vuex-sfc/state/inject/src/app.vue
    <template>
      <p>{{ number }}</p>
    </template>
    
    <script>
    export default {
      computed: {
        number () {
          return this.$store.state.number
        }
      }
    }
    </script>

在更新后的代码中,store 现在被注入到子组件中,你可以使用组件中的 this.$store 来访问它。然而,当你有很多 store 的 state 属性需要通过计算属性来计算时,这种模式可能会变得重复且冗长。在这种情况下,我们可以使用 mapState 辅助函数来减轻负担。让我们在下一节中看看如何使用它。

mapState 辅助函数

我们可以使用 mapState 辅助函数来帮助我们生成计算状态的函数,从而节省一些代码行和按键次数,步骤如下:

  1. 创建一个包含多个 state 属性的 store:

    // vuex-sfc/state/mapstate/src/entry.js
    import Vue from 'vue/dist/vue.js'
    import App from './app.vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: {
        experience: 1,
        name: 'John',
        age: 20,
        job: 'designer'
      }
    })
  2. 从 Vuex 导入 mapState 辅助函数,并将 state 属性作为数组传递给 mapState 方法:

    // vuex-sfc/state/mapstate/src/app.vue
    import { mapState } from 'vuex'
    
    export default {
      computed: mapState([
        'experience', 'name', 'age', 'job'
      ])
    }

    只要映射的计算属性的名称与 state 属性的名称相同,这种方式就能完美工作。然而,最好将其与对象展开运算符一起使用,这样我们就可以在 computed 属性中混合使用多个 mapState 辅助函数:

    computed: {
      ...mapState({
        // ...
      })
    }

    例如,你可能想将 state 数据与子组件中的数据一起计算,如下所示:

    // vuex-sfc/state/mapstate/src/app.vue
    import { mapState } from 'vuex'
    
    export default {
      data () {
        return { localExperience: 2 }
      },
      computed: {
        ...mapState([
          'experience', 'name', 'age', 'job'
        ]),
        ...mapState({
          experienceTotal (state) {
            return state.experience + this.localExperience
          }
        })
      }
    }

    你还可以传递一个字符串值来为 experience state 属性创建一个别名,如下所示:

    ...mapState({
      experienceAlias: 'experience'
    })
  3. 将计算出的 state 属性添加到 <template> 中,如下所示:

    // vuex-sfc/state/mapstate/src/app.vue
    <template>
      <p>{{ name }}, {{ age }}, {{ job }}</p>
      <p>{{ experience }}, {{ experienceAlias }}, {{ experienceTotal }}</p>
    </template>

    你应该在浏览器上看到以下结果:

    John, 20, designer
    1, 1, 3

你可能想知道,既然我们可以在子组件中计算 state 数据,那么我们可以在 store 本身中计算 state 数据吗?答案是肯定的,我们可以使用 getters 来做到这一点,我们将在下一节中介绍 getters。让我们开始吧。

Getters

你可以在 store 的 getters 属性中定义 getter 方法,以便在子组件的视图中使用 state 之前对其进行计算。与计算属性类似,getter 中的计算结果也是响应式的,但它会被缓存,并且只有在其依赖项发生更改时才会更新。一个 getter 接收 state 作为其第一个参数,getters 作为其第二个参数。让我们按照以下步骤创建一些 getter 并在子组件中使用它们:

  1. 创建一个 store,其中包含一个包含项目列表的 state 属性以及一些用于访问这些项目的 getter:

    // vuex-sfc/getters/basic/src/entry.js
    import Vue from 'vue/dist/vue.js'
    import App from './app.vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: {
        fruits: [
          { name: 'strawberries', type: 'berries' },
          { name: 'orange', type: 'citrus' },
          { name: 'lime', type: 'citrus' }
        ]
      },
      getters: {
        getCitrus: state => {
          return state.fruits.filter(fruit => fruit.type === 'citrus')
        },
        countCitrus: (state, getters) => {
          return getters.getCitrus.length
        },
        getFruitByName: (state, getters) => (name) => {
          return state.fruits.find(fruit => fruit.name === name)
        }
      }
    })
    
    new Vue({
      el: 'app',
      template: '<App/>',
      store,
      components: { App }
    })

    在这个 store 中,我们创建了 getCitrus 方法来获取所有类型为 citrus 的项目,以及依赖于 getCitrus 方法结果的 countCitrus 方法。第三个方法 getFruitByName 用于按名称获取列表中的特定水果。

  2. 在组件的 computed 属性中创建一些方法来执行 store 中的 getter,如下所示:

    // vuex-sfc/getters/basic/src/app.vue
    export default {
      computed: {
        totalCitrus () {
          return this.$store.getters.countCitrus
        },
        getOrange () {
          return this.$store.getters.getFruitByName('orange')
        }
      }
    }
  3. 将计算出的 state 属性添加到 <template> 中,如下所示:

    // vuex-sfc/getters/basic/src/app.vue
    <template>
      <p>{{ totalCitrus }}</p>
      <p>{{ getOrange }}</p>
    </template>

    你应该在浏览器中看到以下结果:

    2
    { "name": "orange", "type": "citrus" }

与 mapState 辅助函数类似,我们可以在计算(computed)属性中使用 mapGetters 辅助函数,它可以帮助我们节省一些代码行和按键次数。让我们在下一节中介绍它。

mapGetters 辅助函数

就像 mapState 辅助函数一样,我们可以使用 mapGetters 辅助函数将 store 中的 getters 映射到计算属性中。让我们看看如何通过以下步骤使用它:

  1. 从 Vuex 导入 mapGetters 辅助函数,并将 getters 作为数组传递给 mapGetters 方法,并使用对象展开运算符,以便我们可以在计算属性中混合使用多个 mapGetters 辅助函数:

    // vuex-sfc/getters/mapgetters/src/app.vue
    import { mapGetters } from 'vuex'
    export default {
      computed: {
        ...mapGetters([
          'countCitrus'
        ]),
        ...mapGetters({
          totalCitrus: 'countCitrus'
        })
      }
    }

    在上面的代码中,我们通过将字符串值传递给 totalCitrus 键,为 countCitrus getter 创建了一个别名。请注意,使用对象展开运算符,我们还可以在计算属性中混合其他普通的 methods。因此,让我们在这些 mapGetters 辅助函数的顶部,向 computed 选项添加一个普通的 getOrange getter method,如下所示:

    // vuex-sfc/getters/mapgetters/src/app.vue
    export default {
      computed: {
        ...mapGetters([
          'countCitrus'
        ]),
        ...mapGetters({
          totalCitrus: 'countCitrus'
        }),
        getOrange () {
          return this.$store.getters.getFruitByName('orange')
        }
      }
    }
  2. 将计算出的 state 属性添加到 <template> 中,如下所示:

    // vuex-sfc/getters/mapgetters/src/app.vue
    <template>
      <p>{{ countCitrus }}</p>
      <p>{{ totalCitrus }}</p>
      <p>{{ getOrange }}</p>
    </template>

    你应该在浏览器上看到以下结果:

    2
    2
    { "name": "orange", "type": "citrus" }

到目前为止,你已经学习了如何使用计算方法和 getters 访问 store 中的 state。那么如何更改 state 呢?让我们在下一节中介绍它。

Mutations

正如我们在之前的章节中提到的,store 的 state 必须通过 mutations 显式地提交(commit)来改变。一个 mutation 只是一个函数,就像你在 store 属性中学到的任何其他函数一样,但它必须在 store 的 mutations 属性中定义。它总是将 state 作为第一个参数。让我们按照以下步骤创建一些 mutations 并在子组件中使用它们:

  1. 创建一个包含 state 属性和一些可用于改变 state 的 mutation 方法的 store,如下所示:

    // vuex-sfc/mutations/basic/src/entry.js
    import Vue from 'vue/dist/vue.js'
    import App from './app.vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: { number: 1 },
      mutations: {
        multiply (state) {
          state.number = state.number * 2
        },
        divide (state) {
          state.number = state.number / 2
        },
        multiplyBy (state, n) {
          state.number = state.number * n
        }
      }
    })
    
    new Vue({
      el: 'app',
      template: '<App/>',
      store,
      components: { App }
    })
  2. 在组件中创建以下方法,通过使用 this.$store.commit 调用 commit 来提交 mutation:

    // vuex-sfc/mutations/basic/src/app.js
    export default {
      methods: {
        multiply () {
          this.$store.commit('multiply')
        },
        multiplyBy (number) {
          this.$store.commit('multiplyBy', number)
        },
        divide () {
          this.$store.commit('divide')
        }
      }
    }

与 getter 方法类似,你也可以在 mutation 方法上使用 mapMutations 辅助函数,让我们在下一节中介绍它。

mapMutations 辅助函数

我们可以使用 mapMutations 辅助函数将组件方法映射到 mutation 方法,并使用对象展开运算符,这样我们就可以在 methods 属性中混合使用多个 mapMutations 辅助函数。让我们看看如何通过以下步骤实现:

  1. 从 Vuex 导入 mapMutations 辅助函数,并将 mutations 作为数组传递给 mapMutations 方法,并使用对象展开运算符,如下所示:

    // vuex-sfc/mutations/mapmutations/src/app.vue
    import { mapMutations } from 'vuex'
    export default {
      computed: {
        number () {
          return this.$store.state.number
        }
      },
      methods: {
        ...mapMutations([
          'multiply',
          'multiplyBy',
          'divide'
        ]),
        ...mapMutations({
          square: 'multiply'
        })
      }
    }
  2. 将计算出的 state 属性和方法添加到 <template> 中,如下所示:

    // vuex-sfc/mutations/mapmutations/src/app.vue
    <template>
      <p>{{ number }}</p>
      <p>
        <button v-on:click="multiply">x 2</button>
        <button v-on:click="divide">/ 2</button>
        <button v-on:click="square">x 2 (square)</button>
        <button v-on:click="multiplyBy(10)">x 10</button>
      </p>
    </template>

你应该看到,当你在浏览器上点击上述按钮时,number state 会响应式地被乘或除。在这个例子中,我们已经成功地通过 mutations 改变了 state 的值,这是 Vuex 的规则之一。另一个规则是我们不能在 mutations 中进行异步调用。换句话说,mutations 必须是同步的,这样 DevTool 就可以记录每个 mutation 以进行调试。如果你想进行异步调用,请使用 actions,我们将在下一节中引导你完成 actions 的学习。让我们开始吧。

Actions

Actions 就像 mutations 一样是函数,但它们不用于改变 state,而是用于提交 mutations。与 mutations 不同,actions 可以是异步的。我们在 store 的 actions 属性中创建 action 方法。一个 action 方法将其 context 对象作为第一个参数,你的自定义参数作为第二个参数,依此类推。你可以使用 context.commit 来提交一个 mutation,使用 context.state 来访问 state,以及使用 context.getters 来访问 getters。让我们按照以下步骤添加一些 action 方法:

  1. 创建一个包含 state 属性和 action 方法的 store,如下所示:

    // vuex-sfc/actions/basic/src/entry.js
    import Vue from 'vue/dist/vue.js'
    import App from './app.vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      state: { number: 1 },
      mutations: {
        multiply (state) {
          state.number = state.number * 2
        },
        divide (state) {
          state.number = state.number / 2
        },
        multiplyBy (state, n) {
          state.number = state.number * n
        }
      },
      actions: {
        multiplyAsync (context) {
          setTimeout(() => {
            context.commit('multiply')
          }, 1000)
        },
        multiply (context) {
          context.commit('multiply')
        },
        multiplyBy (context, n) {
          context.commit('multiplyBy', n)
        },
        divide (context) {
          context.commit('divide')
        }
      }
    })
    
    new Vue({
      el: 'app',
      template: '<App/>',
      store,
      components: { App }
    })

    在这个例子中,我们使用了上一节中相同的 mutations,并创建了 action 方法,其中一个创建了一个异步 action 方法来演示为什么我们需要 actions 进行异步调用,即使它们乍一看似乎很繁琐。

    请注意,如果你愿意,可以使用 ES6 JavaScript 的解构赋值来解构 context,并直接导入 commit 属性,如下所示:

    divide ({ commit }) {
      commit('divide')
    }
  2. 创建一个组件并使用 this.$store.dispatch 来 dispatch 上述 actions,如下所示:

    // vuex-sfc/actions/basic/src/app.js
    export default {
      methods: {
        multiply () {
          this.$store.dispatch('multiply')
        },
        multiplyAsync () {
          this.$store.dispatch('multiplyAsync')
        },
        multiplyBy (number) {
          this.$store.dispatch('multiplyBy', number)
        },
        divide () {
          this.$store.dispatch('divide')
        }
      }
    }

与 mutation 和 getter 方法类似,你也可以在 action 方法上使用 mapActions 辅助函数,让我们在下一节中介绍它。

mapActions 辅助函数

我们可以使用 mapActions 辅助函数将组件方法映射到 action 方法,并使用对象展开运算符,这样我们就可以在 methods 属性中混合使用多个 mapActions 辅助函数。让我们看看如何通过以下步骤实现:

  1. 从 Vuex 导入 mapActions 辅助函数,并将 mutations 作为数组传递给 mapActions 方法,并使用对象展开运算符,如下所示:

    // vuex-sfc/actions/mapactions/src/app.vue
    import { mapActions } from 'vuex'
    export default {
      methods: {
        ...mapActions([
          'multiply',
          'multiplyAsync',
          'multiplyBy',
          'divide'
        ]),
        ...mapActions({
          square: 'multiply'
        })
      }
    }
  2. 将计算出的 state 属性并将方法绑定到 <template>,如下所示:

    // vuex-sfc/mapactions/src/app.vue
    <template>
      <p>{{ number }}</p>
      <p>
        <button v-on:click="multiply">x 2</button>
        <button v-on:click="square">x 2 (square)</button>
        <button v-on:click="multiplyAsync">x 2 (multiplyAsync)</button>
        <button v-on:click="divide">/ 2</button>
        <button v-on:click="multiplyBy(10)">x 10</button>
      </p>
    </template>
    
    <script>
    export default {
      computed: {
        number () {
          return this.$store.state.number
        }
      }
    }
    </script>

你应该看到,当你在浏览器上点击上述按钮时,number state 会响应式地被乘或除。在这个例子中,我们再次通过 actions 提交 mutations 来改变了 state 的值,而 actions 只能通过使用 store 的 dispatch 方法来触发。这些是我们在应用程序中使用 store 时必须遵守的强制规则。

然而,当 store 和应用程序变大时,我们可能希望将 state、mutations 和 actions 分成不同的组。在这种情况下,我们将需要 Vuex 的最后一个概念——modules——我们将在下一节中介绍它。让我们开始吧。

Modules

我们可以将我们的 store 分成模块来扩展应用程序。每个模块都可以拥有自己的 state、mutations、actions 和 getters,如下所示:

const module1 = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const module2 = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

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

然后你可以像这样访问每个模块的 state 或其他属性:

store.state.a
store.state.b

在为你的 store 编写模块时,你应该理解模块中的局部 state、根 state 和命名空间。让我们在接下来的章节中看看它们。

理解局部 state 和根 state

每个模块中的 mutations 和 getters 会将其模块的局部 state 作为它们的第一个参数接收,如下所示:

const module1 = {
  state: { number: 1 },
  mutations: {
    multiply (state) {
      console.log(state.number)
    }
  },
  getters: {
    getNumber (state) {
      console.log(state.number)
    }
  }
}

在这段代码中,mutation 和 getter 方法中的 state 是局部模块 state,因此 console.log(state.number) 会输出 1。而在每个模块的 actions 中,你会将 context 作为第一个参数接收,你可以使用 context.state 访问局部 state,使用 context.rootState 访问根 state,如下所示:

const module1 = {
  actions: {
    doSum ({ state, commit, rootState }) {
      //...
    }
  }
}

根 state 也可以在每个模块的 getters 中作为第三个参数使用,如下所示:

const module1 = {
  getters: {
    getSum (state, getters, rootState) {
      //...
    }
  }
}

当有多个模块时,模块中的局部 state 和 store 根部的根 state 可能会混淆并变得难以理解。这就引出了命名空间的概念,它可以使我们的模块更加独立,并且减少相互冲突的可能性。让我们在下一节中介绍它。

理解命名空间

默认情况下,每个模块中的 actions、mutations 和 getters 属性都注册在全局命名空间下,因此每个属性中的键或方法名都必须是唯一的。换句话说,一个方法名不能在两个不同的模块中重复,如下所示:

// entry.js
const module1 = {
  getters: {
    getNumber (state) {
      return state.number
    }
  }
}
const module2 = {
  getters: {
    getNumber (state) {
      return state.number
    }
  }
}

对于上面的示例,由于在 getters 中使用了相同的方法名,你将看到以下错误:

[vuex] duplicate getter key: getNumber

因此,为了避免重复,每个模块的方法名都必须显式命名,如下所示:

getNumberModule1
getNumberModule2

然后,你可以在子组件中访问并映射这些方法,如下所示:

// app.js
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters({
      getNumberModule1: 'getNumberModule1',
      getNumberModule2: 'getNumberModule2'
    })
  }
}

如果你不想像上面的代码那样使用 mapGetters,这些方法也可以写成如下形式:

// app.js
export default {
  computed: {
    getNumberModule1 (state) {
      return this.$store.getters.getNumberModule1
    },
    getNumberModule2 (state) {
      return this.$store.getters.getNumberModule2
    }
  }
}

然而,这种模式可能看起来很冗长,因为对于我们在 store 中创建的每个方法,我们都必须重复地编写 this.$store.getters 或 this.$store.actions。访问每个模块的 state 也是如此,如下所示:

// app.js
export default {
  computed: {
    ...mapState({
      numberModule1 (state) {
        return this.$store.state.a.number
      }
    }),
    ...mapState({
      numberModule2 (state) {
        return this.$store.state.b.number
      }
    })
  }
}

因此,解决这种情况的方法是为每个模块使用命名空间,方法是将每个模块中的 namespaced 键设置为 true,如下所示:

const module1 = {
  namespaced: true
}

当模块被注册时,它的所有 getters、actions 和 mutations 将根据模块注册的路径自动进行命名空间化。看下面的例子:

// entry.js
const module1 = {
  namespaced: true,
  state: { number:1 }
}
const module2 = {
  namespaced: true,
  state: { number:2 }
}
const store = new Vuex.Store({
  modules: {
    a: module1,
    b: module2
  }
})

现在,你可以使用更少的代码且更易读的方式访问每个模块的 state,如下所示:

// app.js
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState('a', {
      numberModule1 (state) {
        return state.number
      }
    }),
    ...mapState('b', {
      numberModule2 (state) {
        return state.number
      }
    })
  }
}

对于上面的示例代码,numberModule1 将得到 1,numberModule2 将得到 2。此外,通过使用命名空间,你还可以消除 “duplicate getter keys” 的错误。因此,现在你可以为方法使用更 “抽象” 的名称,如下所示:

// entry.js
const module1 = {
  namespaced: true,
  getters: {
    getNumber (state) {
      return state.number
    }
  }
}
const module2 = {
  namespaced: true,
  getters: {
    getNumber (state) {
      return state.number
    }
  }
}

现在,你可以使用它们注册的命名空间精确地调用和映射这些方法,如下所示:

// app.js
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters('a', {
      getNumberModule1: 'getNumber',
    }),
    ...mapGetters('b', {
      getNumberModule2: 'getNumber',
    })
  }
}

我们一直在根文件 entry.js 中编写 store。无论你是否编写模块化的 store,当 state 属性以及 mutations、getters 和 actions 中的方法随着时间的推移而增长时,这个根文件都会变得臃肿。因此,这就引出了下一节,你将学习如何将这些方法和 state 属性分离并组织到它们各自的独立文件中。让我们开始吧。