阅读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 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 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) } } }