这是 Vue 全家桶组件系列的文章,梳理一下比较难懂的几个点,此篇文章要说的就是 Vuex
Vuex 是个啥
官方说明: Vuex 是一个专为 Vue.js 应用程序开发的 状态管理模式 ,它采用 集中式存储 来管理应用中所有组件的状态,并以相应的规则保证 状态 以一种可预测的方式发生变化。
好了,道理我都懂,可是这段话究竟是什么意思呢?注意一个词: 状态管理 ,好了,什么是状态,它要怎么去管理?
Vuex 的官网上,有这么段简单的代码说明new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
从中我们可以看到
- 所谓的
state
,其实就是data
- 所谓的
view
,其实就是模板template
- 所谓的
actions
, 其实就是methods
好了,我们现在想想,假如在一个页面中,多个组件需要进行通信,或者说需要一些数据的共享,我们应该怎么做?封装过组件的人都知道,组件通信一般有两种方式
- 对于父子通信,可以使用 props 传递数据,使用 v-on 绑定自定义事件
- 对于爷孙、兄弟通信,可以使用 eventBus
以上都是基于单向数据流的原则,非常之简洁。但是在 多个组件需要共享数据 这个情况下,上面两种方式就不适用了,为什么?
因为你会遇到如下两种情况
- 多个
view
依赖同一个data
- 多个
view
要改变同一个data
对于第一种情况来说,举个栗子,假如我 data
里面有个 count
,只有当 count
大于 0 时,页面上的几个组件才能显示,但是假如有这么个奇葩的需求,就是有个组件它是 嵌套的,必须要最里面那个先显示,外层的才一个个跟着显示,那这里最里面的子组件就必须维护一个 count
,然后父组件就通过 $children
来判断,假如这个嵌套很深,难道你不觉得这样做很 繁琐 很不优雅吗?
对于第二种情况来说,也举个栗子,假如有个极端的需求,子组件里面有个 count
,有个 button
,点击 button
后 count
会变,然后父组件上有三个这样的子组件,需求是当点击任何一个子组件的 button
, 每个子组件的 count
都会变化,请问这种情况下该怎么做?要是后面需求将 count
改成 apple
怎么办?要是后面还增加了一个 xxx
的数据怎么办?这样就不好做代码维护吧。
Vuex 是怎么做的
好了,既然大家都跟这个 状态 有关系,那么为什么不把它抽取出来做一个统一管理呢?其实在 js 中,我们可以很容易的想到用一个 window
来存一个全局变量,比如使用 window.xxx
,那么其他模块就可以访问到这个 xxx
变量了。可是能访问到也不行啊, 要知道 xxx
这个变量是会变的,那么我们怎么能 动态地知道 这个变量的变化呢?很简单,就去 造个事件 ,就是当 xxx
变量有变化的时候通知一下各个模块。
Vuex 做了啥?其实大致就做了上面的事
开始认识 Vuex
一个 Vuex 中,有个叫 Store
的对象,这个对象中,平常用的多的就是如下几个const store = new Vuex.Store({
state,
mutations,
getters,
actions,
});
Store
Store
是个啥呢,你可以把它理解为一个数据管理中心,在这个数据管理中心里,有
- 管理的数据,我们叫它
state
- 管理数据的相关操作,你可以理解为 CUD 都在这里面,我们叫它
mutations
- 获取数据的操作,你可以理解为数据库的 R 操作在这里面,我们叫他
getters
- 通知数据库要管理数据库了,这个操作我们叫它
actions
state
state
就是我们所说的数据了,注意,这里由于使用的是 单一状态树,也就是 唯一数据源, 所以每个应用就也只包含一个 Store
实例,所以它们才能共享这个实例里面的数据,也就是 state
, 比如我们在 state
中有个 count
数据const store = new Vuex.Store({
state: {
count: 0
}
});
在根组件中注入 store
实例const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
然后在组件中访问它const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count // 通过 this.$store.state 访问
}
}
}
getters
获取数据的操作,我们同样可以不使用 this.$store.state
这种方式,而使用 getters
来代替,个人认为这是对前者的一种封装,当然 getters
的特点不止于此,你可以认为它是 Store
的计算属性,所以 getter
是有返回值的(废话),但是这个返回值会被 缓存 起来,只有其依赖发生改变才会重新计算const store = new Vuex.Store({
state: {
count: 0
},
getters: {
doneCount: state => {
return state.count;
}
}
});
然后在组件中使用...
computed: {
count() {
return this.$store.getters.doneCount;
}
}
...
mutations
我们要更改数据,就不要直接通过 this.$store.state
去修改了, Vuex 在这里提供了一个方式 mutations
, 当我触发 commit
某个 type
事件,就会调用 mutations
中已经定义好的 handler
回调函数const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
在组件中你可以通过这样的方式触发this.$store.commit('increament');
commit
还可以传一个额外的参数,这个参数叫 payload 载荷mutations: {
increment (state, n) {
state.count += n
}
}
然后你可以在组件中这样触发this.$store.commit('increment', 10);
你也可以采用对象的方式mutations: {
increment (state, payload) {
state.count += payload.amount;
}
}
然后你可以在组件中这样触发this.$store.commit('increament', {amount: 10});
或者写成这样this.$store.commit({type: 'increament', amount: 10});
actions
就像我之前所说,actions
的作用就是通知 mutations
该去处理数据了,至于处理数据的具体事宜,actions
并不关心,因为那个是 mutations
来负责的,接下来简单写一个 actions
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
然后你就可以在组件对应的 methods
属性中这样写methods: {
add() {
this.$store.dispatch('increament');
}
}
注意,这里 actions
函数中的 context
对象虽然能够使用 commit
方法,但它 并不是 store
实例, 并且 actions
需要通过 store.dispatch
方法来触发
其他的注意点
mutations
里的函数必须是 同步执行 的,而actions
里的函数 可以执行异步操作
actions: { |
然后你就可以这样使用this.$store.dispatch('actionA').then(() => {
// ...
})
同样这里也支持 async/await
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
- 对于表单来说,你可以使用带有 setter 的双向绑定计算属性
// html |
actions
提交的是mutation
,而不是直接变更状态
关于辅助函数 mapXXX
个人认为, Vuex 中 map 函数的作用就是为了 简化代码,比如使用 mapState
函数可以帮你生成计算属性import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
也可以写成computed: mapState([
// 映射 this.count 为 store.state.count
'count',
])
同时若想将属性混入(mixin) computed
,你可以使用 ...
对象展开运算符computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
至于其他的 mapGetters
、mapMutaions
、mapActions
用法都差不多,详细还请查询 官方文档,这里就不再过多赘述