Better

Ethan的博客,欢迎访问交流

Vuex使用

阅读vuex官方文档的总结

简介

状态管理模式:它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

背景:当应用遇到多个组件共享状态时,比如:

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

如果不使用vuex,我们如何解决上面的问题呢,对于问题一,组件之间使用依赖传参的方式,但这样多层组件时会十分麻烦。对于问题二,采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。但这样通常回导致代码无法维护。

因此我们想到把组件的共享状态抽取出来,这样任何组件都能获取状态或者触发行为。因此vuex就应运而生了。

什么情况下选择使用vuex

  • vuex可以帮助我们共享状态,但是附带了更多的概念和框架
  • 如果应用够简单,使用 Vuex 可能是繁琐冗余的。一个简单的全局事件总线就可以满足要求了。

引用 Redux 的作者 Dan Abramov 的话说就是:

Flux 架构就像眼镜:您自会知道什么时候需要它。

Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。

简单DEMO如下:

// 定义 mutation-type.js 汇总所有操作类型
vue.use(vuex)
const store = new vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    [ADD_COUNT](state) {
      state.count++
    }
  }
})
export default store

// 如果其他组件中需要使用store,则均需要import store,我们可以直接在根Vue中注册,这样依赖我们在组件中均可以通过 this.$store 访问
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})
// 由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。
export default {
    computed:{
        count(){
            return this.$store.state.count
        }
    },
    methods: {
        add() {
        this.$store.commit(ADD_COUNT)
        }
    }
}

核心概念

State

单一状态树:用一个对象就包含了全部的应用层级状态。作为一个“唯一数据源 (SSOT)”而存在。

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

computed:mapState({
    count:state => state.count,
    countAlias:'count',
    // 为了可以使用this得到组件,必须使用常规函数
    countPlusLocalState(state){
        return state.count + this.localCount
    }
})

mapState 直接返回一个对象, 如何需要将它与局部计算属性混用,只用...扩展符即可

Getter

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数,Getter 也可以接受其他 getter 作为第二个参数,方便你调用其他属性。你还可以让 getter 返回一个函数,实现给 getter 传参。

getters:{
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    },
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    },
    getTodoById: (state) => (id) => {
      return state.todos.find(todo => todo.id === id)
    }
}
// Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值
this.$store.getters.doneTodosCount

mapGetters 帮助我们批量映射到 computed 中,参数可以是数组或对象,对象的方式可以帮助我们取别名。

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。

你不能直接调用一个 mutation handler。如同事件一样,你需要 store.commit 方法去调用相应的 type 的 mutation。

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload)

可以使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

methods:{
    ...mapMutations([
        'increment' // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
    ]),
    ...mapMutations({
        add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
}

响应规则

既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项

  • 最好提前在你的 store 中初始化好所有所需属性。
  • 当需要在对象上添加新属性时,你应该使用 Vue.set(obj, 'newProp', 123), 或者以新对象替换老对象

注意事项:Mutation 必须是同步函数

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务

Action

Action 类似于 mutation,不同在于:

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

Action 通过 store.dispatch 方法触发。同样支持负载 payload。

同样可以使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store),使用和 mapMutations 类似。

由于 Action 是处理异步的情况,因此就涉及到 Action 组合的问题,比如一个 Action 需要等到另一个 Action 执行完毕呢。我们可以在 Action 中返回 Promise 来达到目的。

actions:{
    actionA(){
        return Promise()
    },
    actionB({dispatch,commit}){
        return dispatch('actionA').then(() => {
            commit('mutation-type')
        })
    }
}

Module

从最开头的例子来看,难道我们一开始就将所有的state、mutations、actions、getters都定义在一个文件中,那样是不是太庞大了,可不可以细分呢?当然可以,vuex.Store构造函数配置参数提供 modules 属性帮助我们模块化。

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
    //...
  }
})
store.state.a // -> moduleA 的状态

局部状态

  • 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
  • 对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
  • 对于模块内部的 getter,rootState 和 rootGetter 会作为第三和第四参数传入 getter

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在 全局命名空间 的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。modules 还可以嵌套。

启用了命名空间的 getter 和 action 就只会收到局部化的 getter,dispatch 和 commit。按如果需要全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

// 在模块 foo 内
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'

在带命名空间的模块注册全局 action,修改 action 的定义如下:

actions: {
    someAction: {
        root: true,
        handler (namespacedContext, payload) { ... } // -> 'someAction'
    }
}

当启用命名空间后,当使用 mapState, mapGetters, mapActions 和 mapMutations 这些函数来绑定带命名空间的模块时,就会比较麻烦。此时我们可以启用 mapXXX 函数两个参数的形式。第一个参数传递模块的空间名称字符串。比如如下

methods: {
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}

如果觉得这样还是不够美观,而且这个空间名称我要写多次,因此我们还可以使用 createNamespacedHelpers 辅助函数

import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

模块动态注册

在 store 创建之后,你可以使用 store.registerModule 方法注册模块:

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync 插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。

你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('a', module, { preserveState: true })。

模块重用

我们可能需要创建一个模块的多个实例

  • 创建多个 store,他们公用同一个模块
  • 在一个 store 中多次注册同一个模块

我们重用模块的目的是重用 mutation, action 和 getter,state 肯定不是一样的,解决办法和 Vue 组件一样,使用一个函数声明模块状态

进阶

项目结构

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

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

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

插件

vuex 支持插件系统去扩展功能,在 new Vuex.Store 构造函数中配置属性 plugins 中注册。

我们可以引入第三方插件,也可以自定义插件,基本模版为

const myPlugin = store => {
    store.subscribe((mutation,state) => {
        console.log(mutation,state);
    })
}
export default myPlugin

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

表单处理

由于 vuex 要求状态的改变必须通过 mutation,这样一来在属于 Vuex 的 state 上使用 v-model 会比较棘手,解决办法有

  • <input> 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用 mutation
  • 上述做法比较啰嗦,而且损失了一些 v-model 中很有用的特性,另一个方法是使用带有 setter 的双向绑定计算属性
    computed: {
    message: {
      get () {
        return this.$store.state.obj.message
      },
      set (value) {
        this.$store.commit('updateMessage', value)
      }
    }
    }
    


留言