理解 Vuex 的核心概念

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

State

StateVuex 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 来访问它。然而,当你有很多 storestate 属性需要通过计算属性来计算时,这种模式可能会变得重复且冗长。在这种情况下,我们可以使用 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

你可以在 storegetters 属性中定义 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

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

  1. 创建一个包含 state 属性和一些可用于改变 statemutation 方法的 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 可以是异步的。我们在 storeactions 属性中创建 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.dispatchdispatch 上述 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')
        }
      }
    }

mutationgetter 方法类似,你也可以在 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 只能通过使用 storedispatch 方法来触发。这些是我们在应用程序中使用 store 时必须遵守的强制规则。

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

Modules

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

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

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

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

在这段代码中,mutationgetter 方法中的 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) {
      //...
    }
  }
}

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

理解命名空间

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

// 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.gettersthis.$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
}

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

// 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 属性以及 mutationsgettersactions 中的方法随着时间的推移而增长时,这个根文件都会变得臃肿。因此,这就引出了下一节,你将学习如何将这些方法和 state 属性分离并组织到它们各自的独立文件中。让我们开始吧。