什么是 Vuex

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

  • Vuex 是专门为 Vue.js 设计的状态管理库
  • 它采用集中式的方式存储需要共享的数据
  • 从使用角度,它就是一个 JavaScript 库
  • 它的作用是进行状态管理,解决复杂组件通信,数据共享

什么情况下使用 Vuex

官方文档:
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:Flux 架构就像眼镜:您自会知道什么时候需要它。

当你的应用中具有以下需求场景的时候:

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态

建议符合这种场景的业务使用 Vuex 来进行数据管理,例如非常典型的场景:购物车。

Vuex - 图1

基本结构

  • 导入 Vuex
  • 注册 Vuex
  • 注入 $store 到 Vue 实例

State

Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。
通过$store.state可以访问到 State 的属性。比如$store.state.count
使用 mapState 简化 State 在视图中的使用,mapState 返回计算属性。

  1. export default new Vuex.Store({
  2. state: {
  3. count: 0,
  4. msg: 'Hello Vuex'
  5. },
  6. })

mapState 有两种使用方式:

  • 接收数组参数

    1. // 该方法是 vuex 提供的,所以使用前要先导入
    2. import { mapState } from 'vuex'
    3. // mapState 返回名称为 count 和 msg 的计算属性
    4. // 在模板中直接使用 count 和 msg
    5. computed: {
    6. ...mapState(['count', 'msg']),
    7. }
  • 接收对象参数
    如果当前视图中已经有了 count 和 msg,如果使用上述方式的话会有命名冲突,解决的方式:

    1. // 该方法是 vuex 提供的,所以使用前要先导入
    2. import { mapState } from 'vuex'
    3. // 通过传入对象,可以重命名返回的计算属性
    4. // 在模板中直接使用 num 和 message
    5. computed: {
    6. // ...mapState({ num: 'count', message: 'msg' }),
    7. ...mapState({
    8. num: state => state.count,
    9. message: state => state.msg
    10. })
    11. }

Getters

Getters 就是 store 中的计算属性,通过$store.getters可以在视图中访问到,使用 mapGetters 简化视图中的使用。

  1. export default new Vuex.Store({
  2. state: {
  3. count: 0,
  4. msg: 'Hello Vuex'
  5. },
  6. getters: {
  7. // 反转 msg 字符串
  8. reverseMsg (state) {
  9. return state.msg.split('').reverse().join('')
  10. }
  11. }
  12. })
  1. import { mapGetters } from 'vuex'
  2. computed: {
  3. ...mapGetters(['reverseMsg']),
  4. // 改名,在模板中使用 reverse
  5. ...mapGetters({
  6. reverse: 'reverseMsg'
  7. })
  8. }

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每 个 mutation 都有一个字符串的 事件类型 (type) 和 一个回调函数 (handler)。这个回调函数就是我们 实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
使用 Mutations 改变状态的好处是,集中的一个位置对状态修改,不管在什么地方修改,都可以追踪到状态的修改。配合官方的 dev tools 可以实现高级的 time-travel 调试功能。
通过$store.commit(mutation_name: string, ...args: [])可以在视图中访问到对应的 mutation,使用 mapMutations 简化在视图中的使用。

  1. export default new Vuex.Store({
  2. state: {
  3. count: 0,
  4. msg: 'Hello Vuex'
  5. },
  6. mutations: {
  7. increate (state, payload) {
  8. state.count += payload
  9. }
  10. }
  11. })
  1. <!-- <button @click="$store.commit('increate', 2)">Mutation</button> -->
  2. <button @click="increate(3)">Mutation</button>
  3. <script>
  4. import { mapMutations } from 'vuex'
  5. methods: {
  6. ...mapMutations(['increate']),
  7. // 传对象解决重名的问题
  8. ...mapMutations({
  9. increateMut: 'increate'
  10. })
  11. }
  12. </script>

Actions

Action 类似于 mutation,不同在于:

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

修改状态先通过 Actions 进行异步处理,再提交给 Mutations。
通过$store.dispatch(action_name: string, ...args: [])可以在视图中访问到对应的 action,使用 mapActions 可以简化在视图中的使用。

  1. export default new Vuex.Store({
  2. state: {
  3. count: 0,
  4. msg: 'Hello Vuex'
  5. },
  6. mutations: {
  7. increate (state, payload) {
  8. state.count += payload
  9. }
  10. },
  11. actions: {
  12. increateAsync (context, payload) {
  13. setTimeout(() => {
  14. context.commit('increate', payload)
  15. }, 2000)
  16. }
  17. }
  18. })
  1. <!-- <button @click="$store.dispatch('increateAsync', 5)">Action</button> -->
  2. <button @click="increateAsync(6)">Action</button>
  3. <script>
  4. import { mapActions } from 'vuex'
  5. methods: {
  6. .....mapActions(['increateAsync']),
  7. // 传对象解决重名的问题
  8. ...mapActions({
  9. increateAction: 'increateAsync'
  10. })
  11. }
  12. </script>

Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、 mutation、action、getter、甚至是嵌套子模块。
默认情况下,模块自己的 state、mutation等都会挂载到 store 上。$store.state下会新增一个对象,名字和模块名相同,对象里存放模块的 state。模块的 action、 mutation、getter 则直接挂载到 store 上,如果模块和 store 有相同的 action、 mutation、getter 时,会共存。为了模块有更好的封装性,可以给模块设置命名空间,namespaced: true
直接访问模块:访问模块的 state 需要加上模块名:$store.state.模块名.name,如果访问像直接挂载在 store 的 mutation,方式和非模块相同。
使用 map 方法访问模块,需要传入模块名。

.\store\modules\products.js:

  1. const state = {
  2. products: [
  3. { id: 1, title: 'iPhone 11', price: 8000 },
  4. { id: 2, title: 'iPhone 12', price: 10000 }
  5. ]
  6. }
  7. const getters = {}
  8. const mutations = {
  9. setProducts (state, payload) {
  10. state.products = payload
  11. }
  12. }
  13. const actions = {}
  14. export default {
  15. namespaced: true,
  16. state,
  17. getters,
  18. mutations,
  19. actions
  20. }

.\src\modules\cart.js

  1. const state = {}
  2. const getters = {}
  3. const mutations = {}
  4. const actions = {}
  5. export default {
  6. namespaced: true,
  7. state,
  8. getters,
  9. mutations,
  10. actions
  11. }
  1. import products from './modules/products'
  2. import cart from './modules/cart'
  3. export default new Vuex.Store({
  4. modules: {
  5. products,
  6. cart
  7. }
  8. })
  1. <!-- products: {{ $store.state.products.products }} <br>
  2. <button @click="$store.commit('setProducts', [])">Mutation</button> -->
  3. products: {{ products }} <br>
  4. <button @click="setProducts([])">Mutation</button>
  5. <script>
  6. computed: {
  7. ...mapState('products', ['products'])
  8. },
  9. methods: {
  10. ...mapMutations('products', ['setProducts'])
  11. }
  12. </script>

严格模式

开启严格模式后,要修改 state 必须经过 mutation。虽然还能直接修改,但会抛出错误。建议在开发环境下开启严格模式,因为严格模式会深度检查状态树,影响性能。

  1. export default new Vuex.Store({
  2. strict: process.env.NODE_ENV !== 'production'
  3. })

购物车案例

接下来我们通过一个购物车案例来演示 Vuex 在项目中的使用方式,首先把购物车的项目模板下载下来。
地址:https://github.com/goddlts/vuex-cart-demo-template.git

功能列表

  • 商品列表组件
  • 商品列表中弹出框组件
  • 购物车列表组件

商品列表

商品列表功能

  • Vuex 中创建两个模块,分别用来记录商品列表和购物车的状态,store 的结构:

image.png

  • products 模块,store/modules/products.js

    1. import axios from 'axios'
    2. const state = {
    3. products: []
    4. }
    5. const getters = {}
    6. const mutations = {
    7. setAllProducts(state, products) {
    8. state.products = products
    9. }
    10. }
    11. const actions = {
    12. async getAllProducts({ commit }) {
    13. const { data } = await axios({
    14. method: 'GET',
    15. url: 'http://127.0.0.1:3000/products'
    16. })
    17. commit('setAllProducts', data)
    18. }
    19. }
    20. export default {
    21. namespaced: true,
    22. state,
    23. getters,
    24. mutations,
    25. actions
    26. }
  • store/index.js 中注册 products.js 模块

  • views/products.vue 中实现商品列表的功能
    1. import { mapState, mapActions } from 'vuex'
    2. export default {
    3. name: 'ProductList',
    4. computed: {
    5. ...mapState('products', ['products'])
    6. },
    7. methods: {
    8. ...mapActions('products', ['getAllProducts'])
    9. },
    10. created() {
    11. this.getAllProducts()
    12. }
    13. }

添加购物车

  • cart 模块实现添加购物车功能,store/modules/cart.js

    1. const mutations = {
    2. addToCart(state, product) {
    3. // 判断 cartProducts 中是否已有该商品
    4. // 如果有的话,把 product 添加到数组中,设置 count=1, isChecked=true, totalPrice
    5. // 否则的话找到购物车中的商品让 count + 1
    6. const prod = state.cartProducts.find(item => item.id === product.id)
    7. if (prod) {
    8. prod.count++
    9. prod.isChecked = true
    10. prod.totalPrice = prod.price * prod.count
    11. } else {
    12. state.cartProducts.push({
    13. // { id, title, price }
    14. ...product,
    15. totalPrice: product.price,
    16. count: 1,
    17. isChecked: true
    18. })
    19. }
    20. }
    21. }
  • store/index.js 中注册 cart 模块

  • view/products.vue 中实现添加购物车功能
    1. <!-- 修改模板 -->
    2. <template slot-scope="scope">
    3. <el-button @click="addToCart(scope.row)">加入购物车</el-button>
    4. </template>
    5. <!-- 映射 cart 中的 mutations -->
    6. ...mapMutations('cart', ['addToCart']),

商品列表 - 弹出购物车窗口

购物车列表

  • components/pop-cart.vue 中展示购物车列表
    1. import { mapState } from 'vuex'
    2. export default {
    3. name: 'PopCart',
    4. computed: {
    5. ...mapState('cart', ['cartProducts'])
    6. }
    7. }

删除

  • cart 模块实现从购物车删除的功能,store/modules/cart.js

    1. // mutations 中添加
    2. deleteFromCart (state, prodId) {
    3. const index = state.cartProducts.find(item => item.id === prodId)
    4. index !== -1 && state.cartProducts.splice(index, 1)
    5. }
  • components/pop-cart.vue 中实现删除功能

    1. <template slot-scope="scope">
    2. <el-button
    3. @click="deleteFromCart(scope.row.id)"
    4. size="mini"
    5. >删除</el-button>
    6. </template>
    1. methods: {
    2. ...mapMutations('cart', ['deleteFromCart'])
    3. }

小计

  • cart 模块实现统计总数和总价,store/modules/cart.js

    1. const getters = {
    2. totalCount(state) {
    3. return state.cartProducts.reduce((count, prod) => {
    4. return count + prod.count
    5. }, 0)
    6. },
    7. totalPrice(state) {
    8. return state.cartProducts.reduce((count, prod) => {
    9. return count + prod.totalPrice
    10. }, 0).toFixed(2)
    11. }
    12. }
  • components/pop-cart.vue 中显示徽章和小计

    1. <div>
    2. <p>共 {{ totalCount }} 件商品 共计¥{{ totalPrice }}</p>
    3. <el-button size="mini" type="danger" @click="$router.push({ name: 'cart' })">
    4. 去购物车</el-button>
    5. </div>
    6. <el-badge :value="totalCount" class="item" slot="reference">
    7. <el-button type="primary">我的购物车</el-button>
    8. </el-badge>
    1. computed: {
    2. ...mapState('cart', ['cartProducts']),
    3. ...mapGetters('cart', ['totalCount', 'totalPrice'])
    4. },

购物车

全选功能

  • cart 模块实现更新商品的选中状态,store/modules/cart.js

    1. // 更新所有商品的选中状态(点击全选)
    2. updateAllProductsChecked(state, checked) {
    3. state.cartProducts.forEach(prod => {
    4. prod.isChecked = checked
    5. })
    6. },
    7. // 更新某个商品的选中状态(点击单个商品)
    8. updateProductChecked(state, {
    9. prodId,
    10. checked
    11. }) {
    12. const prod = state.cartProducts.find(prod => prod.id === prodId)
    13. prod && (prod.isChecked = checked)
    14. }
  • views/cart.vue,实现全选功能

    1. <el-checkbox slot="header" slot-scope="scope" size="mini" v-model="checkedAll">
    2. </el-checkbox>
    3. <!--
    4. @change="updateProductChecked" 默认参数:更新后的值
    5. @change="updateProductChecked(productId, $event)" 123, 原来那个默认参数
    6. 当你传递了自定义参数的时候,还想得到原来那个默认参数,就手动传递一个 $event
    7. -->
    8. <template slot-scope="scope">
    9. <el-checkbox
    10. size="mini"
    11. :value="scope.row.isChecked"
    12. @change="updateProductChecked({
    13. prodId: scope.row.id,
    14. checked: $event
    15. })"
    16. >
    17. </el-checkbox
    18. ></template>
    1. computed: {
    2. ...mapState('cart', ['cartProducts']),
    3. checkedAll: {
    4. get() {
    5. return this.cartProducts.every(prod => prod.isChecked)
    6. },
    7. set(value) {
    8. this.updateAllProductsChecked(value)
    9. }
    10. }
    11. },
    12. methods: {
    13. ...mapMutations('cart', [
    14. 'updateAllProductsChecked',
    15. 'updateProductChecked'
    16. ])
    17. }

数字文本框

  • cart 模块实现更新商品数量,store/modules/cart.js

    1. updateProductCount(state, {
    2. prodId,
    3. count
    4. }) {
    5. const prod = state.cartProducts.find(prod => prod.id === prodId)
    6. if (prod) {
    7. prod.count = count
    8. prod.totalPrice = prod.price * count
    9. }
    10. }
  • views/cart.vue,实现数字文本框功能

    1. <template slot-scope="scope">
    2. <el-input-number
    3. size="mini"
    4. :min="1"
    5. controls-position="right"
    6. :value="scope.row.count"
    7. @change="updateProductCount({
    8. prodId: scope.row.id,
    9. count: $event
    10. })"
    11. ></el-input-number>
    12. </template>
    1. ...mapMutations('cart', [
    2. 'updateAllProductsChecked',
    3. 'updateProductChecked',
    4. 'updateProductCount'
    5. ])

小计

  • cart 模块实现统计选中商品价格和数量,store/modules/cart.js
    1. checkedTotalCount(state) {
    2. return state.cartProducts.reduce((count, prod) => {
    3. if (prod.isChecked) {
    4. count += prod.count
    5. }
    6. return count
    7. }, 0)
    8. },
    9. checkedTotalPrice(state) {
    10. return state.cartProducts.reduce((count, prod) => {
    11. if (prod.isChecked) {
    12. count += prod.totalPrice
    13. }
    14. return count
    15. }, 0).toFixed(2)
    16. }
  • views/cart.vue,实现小计
    1. <p>已选 <span>{{ checkedTotalCount }}</span> 件商品,总价:<span>{{
    2. checkedTotalPrice }}</span></p>
    1. ...mapGetters('cart', ['checkedTotalCount', 'checkedTotalPrice'])

Vuex 模拟实现

实现思路

  • 实现 install 方法
    • Vuex 是 Vue 的一个插件,所以和模拟 VueRouter 类似,先实现 Vue 插件约定的 install 方法
  • 实现 Store 类
    • 实现构造函数,接收 options
    • state 的响应化处理
    • getter 的实现
    • commit、dispatch 方法

实现

  1. let _Vue = null
  2. class Store {
  3. constructor (options) {
  4. const {
  5. state = {},
  6. getters = {},
  7. mutations = {},
  8. actions = {}
  9. } = options
  10. this.state = _Vue.observable(state)
  11. // 此处不直接 this.getters = getters,是因为下面的代码中要方法 getters 中的 key
  12. // 如果这么写的话,会导致 this.getters 和 getters 指向同一个对象
  13. // 当访问 getters 的 key 的时候,实际上就是访问 this.getters 的 key 会触发 key 属性
  14. getter
  15. // 会产生死递归
  16. this.getters = Object.create(null)
  17. Object.keys(getters).forEach(key => {
  18. Object.defineProperty(this.getters, key, {
  19. get: () => getters[key](state)
  20. })
  21. })
  22. this._mutations = mutations
  23. this._actions = actions
  24. }
  25. commit (type, payload) {
  26. this._mutations[type](this.state, payload)
  27. }
  28. dispatch (type, payload) {
  29. this._actions[type](this, payload)
  30. }
  31. }
  32. function install (Vue) {
  33. _Vue = Vue
  34. _Vue.mixin({
  35. beforeCreate () {
  36. if (this.$options.store) {
  37. _Vue.prototype.$store = this.$options.store
  38. }
  39. }
  40. })
  41. }
  42. export default {
  43. Store,
  44. install
  45. }