Vue全家桶之Vuex

这是 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,点击 buttoncount 会变,然后父组件上有三个这样的子组件,需求是当点击任何一个子组件的 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: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

然后你就可以这样使用

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
<input v-model="message">

// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
  • 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({
// ...
})
}

至于其他的 mapGettersmapMutaionsmapActions 用法都差不多,详细还请查询 官方文档,这里就不再过多赘述