理解 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 的提取思想相悖。因此,让我们按照以下步骤重构前面的代码:
-
将
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 } }) -
从子组件中移除
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 辅助函数来帮助我们生成计算状态的函数,从而节省一些代码行和按键次数,步骤如下:
-
创建一个包含多个
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' } }) -
从
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' }) -
将计算出的
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 并在子组件中使用它们:
-
创建一个
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用于按名称获取列表中的特定水果。 -
在组件的
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') } } } -
将计算出的
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 映射到计算属性中。让我们看看如何通过以下步骤使用它:
-
从
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键,为countCitrusgetter 创建了一个别名。请注意,使用对象展开运算符,我们还可以在计算属性中混合其他普通的methods。因此,让我们在这些mapGetters辅助函数的顶部,向computed选项添加一个普通的getOrangegetter method,如下所示:// vuex-sfc/getters/mapgetters/src/app.vue export default { computed: { ...mapGetters([ 'countCitrus' ]), ...mapGetters({ totalCitrus: 'countCitrus' }), getOrange () { return this.$store.getters.getFruitByName('orange') } } } -
将计算出的
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 并在子组件中使用它们:
-
创建一个包含
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 } }) -
在组件中创建以下方法,通过使用
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 辅助函数。让我们看看如何通过以下步骤实现:
-
从
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' }) } } -
将计算出的
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 方法:
-
创建一个包含
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') } -
创建一个组件并使用
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 辅助函数。让我们看看如何通过以下步骤实现:
-
从
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' }) } } -
将计算出的
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 属性分离并组织到它们各自的独立文件中。让我们开始吧。