vue 快速入门 系列 —— Vuex 基础

其他章节请看:

vue 快速入门 系列

Vuex 基础

Vuex 是 Vue.js 官方的状态管理器

vue 的基础应用(上)一文中,我们已知道父子之间通信可以使用 props$emit,而非父子组件通信(兄弟、跨级组件、没有关系的组件)使用 bus(中央事件总线)来起到通信的作用。而 Vuex 作为 vue 的一个插件,解决的问题与 bus 类似。bus 只是一个简单的组件,功能也相对简单,而 Vuex 更强大,使用起来也复杂一些。

现在的感觉就是 Vuex 是一个比 bus 更厉害的东西,可以解决组件之间的通信。更具体些,就是 vuex 能解决多个组件共享状态的需求:

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

Vuex 把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。

环境准备

通过 vue-cli 创建项目

// 项目预设 `[Vue 2] less`, `babel`, `router`, `vuex`, `eslint`
$ vue create test-vuex

Tip:环境与Vue-Router 基础相同

核心概念

Vuex 的核心概念有 State、Getters、Mutations、Actions和Modules。

我们先看一下项目 test-vuex 中的 Vuex 代码:

// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // vuex 中的数据
  state: {
  },
  // 更改 vuex 中 state(数据)的唯一方式
  mutations: {
  },
  // 类似 mutation,但不能直接修改 state
  actions: {
  },
  // Vuex 允许将 store 分割成模块(module),每个模块可以拥有自己的 state、mutation、action、getter
  modules: {
  }
})

Getters,可以认为是 store 的计算属性

State

state 是 Vuex 中的数据,类似 vue 中的 data。

需求:在 state 中定义一个属性 isLogin,从 About.vue 中读取该属性。

直接上代码:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.state.isLogin }}</p>
  </div>
</template>

页面输出 true

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)),子组件能通过 this.$store 访问,这样就无需在每个使用 state 的组件中频繁的导入。

// main.js
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
// store/index.js
Vue.use(Vuex)

Tip:Vuex 的状态存储是响应式。

mapState 辅助函数

从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态。

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

// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  computed: mapState([
    // 映射 this.isLogin 为 store.state.isLogin
    'isLogin'
  ])
}
</script>

页面同样输出 true。

Tip:更多特性请看官网

Getters

Getters,可以认为是 store 的计算属性。

getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

需求:从 isLogin 派生出一个变量,从 About.vue 中读取该属性

直接上代码:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
  getters: {
    translationIsLogin: state => {
      return state.isLogin ? '已登录' : '未登录'
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.getters.translationIsLogin }}</p>
  </div>
</template>

页面输出“已登录”

Tip:更多特性请参考官网。

  • 可以给 getter 传参
  • 有与 state 类似的辅助函数,这里是 mapGetters

Mutations

mutation 是更改 vuex 中 state(数据)的唯一方式。

mutation 类似事件,每个 mutation 都有一个字符串的事件类型和 一个回调函数。不能直接调用一个 mutation handler,只能通过 store.commit 方法调用。

需求:定义一个 mutation(更改 isLogin 状态),在 About.vue 中过三秒触发这个 mutation。

直接上代码:

// store/index.js
export default new Vuex.Store({
  state: {
    isLogin: true
  },
  mutations: {
    toggerIsLogin(state) {
      state.isLogin = !state.isLogin
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
export default {
  created() {
    setInterval(()=>{
      this.$store.commit('toggerIsLogin')
    }, 3000)
  },
}
</script>

页面每三秒会依次显示 true -> false -> true ...

Mutation 必须是同步函数
  • 笔者在 mutation 中写异步函数(使用 setTimeout)测试,没有报错
  • 在 mutation 中混合异步调用会导致程序很难调试(使用 devtools)
  • 当调用了两个包含异步回调的 mutation 来改变状态,不知道什么时候回调和哪个先回调

结论:在 mutation 中只使用同步函数,异步操作放在 action 中。

Tip:更多特性请参考官网。

  • 可以给 mutation 传参
  • 触发(commit)方式可以使用对象
  • 有与 state 类似的辅助函数,这里是 mapMutations

Actions

Action 类似于 mutation,不同在于:

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

需求:定义一个 action,里面有个异步操作,过三秒更改 isLogin 状态。

直接上代码:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    isLogin: true
  },
  mutations: {
    toggerIsLogin(state) {
      state.isLogin = !state.isLogin
    }
  },
  actions: {
    toggerIsLogin(context) {
      setInterval(() => {
        context.commit('toggerIsLogin')
      }, 3000)
    }
  },
})
// views/About.vue
<template>
  <div class="about">
    <p>{{ isLogin }}</p>
  </div>
</template>
<script>
export default {
  created() {
    // 通过 dispatch 分发
    this.$store.dispatch('toggerIsLogin')
  },
}
</script>

过三秒,页面的 true 变成 false。

实践中,我们会经常用到 ES2015 的参数解构来简化代码:

actions: {
    toggerIsLogin({ commit }) {
      setInterval(() => {
        commit('toggerIsLogin')
      }, 3000)
    }
  },

Tip:更多特性请参考官网。

  • 可以给 Actions 传参
  • 触发(dispatch)方式可以使用对象
  • 有与 state 类似的辅助函数,这里是 mapActions
  • 组合多个 Action

Modules

目前我们的 store 都写在一个文件中,当应用变得复杂时,store 对象就有可能变得相当臃肿。

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

Vuex 允许将 store 分割成模块(module),每个模块可以拥有自己的 state、mutation、action、getter。

需求:定义两个模块,每个模块定义一个状态,在 About.vue 中显示这两个状态

直接上代码:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const moduleA = {
  state: () => ({ name: 'apple' }),
}

const moduleB = {
  state: () => ({ name: 'orange' }),
}

export default new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
  }
})
// views/About.vue
<template>
  <div class="about">
    <!-- 即使给这两个模块都加上命名空间,这样写也是没问题的 -->
    <p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
  </div>
</template>

页面显示 “apple orange”。

模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。就像这样:

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
}

对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState。就像这样:

const moduleA = {
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}
命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的。

如果希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。请看示意代码:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

小练习

请问 About.vue 会输出什么?(答案在文章底部)

// views/About.vue
<template>
  <div class="about">
    <p>{{ this.$store.state.a.name }} {{ this.$store.state.b.name }}</p>
    <p>
      {{ this.$store.getters.nameA }} {{ this.$store.getters.nameB }}
      {{ this.$store.getters["b/nameB"] }}
    </p>
  </div>
</template>
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const moduleA = {
  namespaced: true,
  state: () => ({ name: 'apple' }),
}

const moduleB = {
  namespaced: true,
  state: () => ({ name: 'orange' }),
  getters: {
    nameB: state => `[${state.name}]`
  }
}

export default new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
  },
  getters: {
    nameA: state => state.a.name,
    nameB: state => state.b.name
  }
})

Tip: 更多特性请参考官网。

项目结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到 action 里面。

只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。

对于大型应用,官网给出了一个项目结构示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

Tip:在笔者即将完成的另一篇文章“使用 vue-cli 3 搭建一个项目”中会有更详细的介绍

附录

小练习答案

apple orange

apple orange [orange]

其他章节请看:

vue 快速入门 系列

posted @ 2021-10-18 10:08  彭加李  阅读(652)  评论(0编辑  收藏  举报