官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。其实,你可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。
那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?
等等,如果是这样的话,为什么官方还要专门出一个插件Vuex呢?
难道我们不能自己封装一个对象来管理吗?当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?
没错,就是响应式。如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。
如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。比如用户的登录状态、用户名称、头像、地理位置信息等等。比如商品的收藏、购物车中的物品等等。这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的(待会儿我们就可以看到代码了,莫着急)。
OK,从理论上理解了状态管理之后,让我们从实际的代码再来看看状态管理。
毕竟,Talk is cheap, Show me the code.(来自Linus)
我们先来看看单界面的状态管理吧.
我们知道,要在单个组件中进行状态管理是一件非常简单的事情
什么意思呢?我们来看下面的图片。
这图片中的三种东西,怎么理解呢?
接下来让我们实现一个简单的单页面状态管理的小demo
在这个案例中,我们有木有状态需要管理呢?没错,就是个数counter。counter需要某种方式被记录下来,也就是我们的State。counter目前的值需要被显示在界面中,也就是我们的View部分。界面发生某些操作时(我们这里是用户的点击,也可以是用户的input),需要去更新状态,也就是我们的Actions。这不就是上面的流程图了吗?
接下来我们来看看多界面状态管理
Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?
多个页面都依赖同一个状态(一个状态改了,多个界面需要进行更新)
不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)
也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个试图,但是也有一些状态(状态a/状态b/状态c)属于多个试图共同想要维护的。
状态1/状态2/状态3你放在自己的房间中,你自己管理自己用,没问题。但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理!!!没错,Vuex就是为我们提供这个大管家的工具。
全局单例模式(大管家)
我们现在要做的就是将共享的状态抽取出来,交给我们的大管家,统一进行管理。
之后,你们每个试图,按照我规定好的规定,进行访问和修改等操作。这就是Vuex背后的基本思想。
Vuex提出使用单一状态树, 什么是单一状态树呢?
英文名称是Single Source of Truth,也可以翻译成单一数据源。
但是,它是什么呢?我们来看一个生活中的例子。
OK,我用一个生活中的例子做一个简单的类比。我们知道,在国内我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录,公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等(还有很多信息)。这些信息被分散在很多地方进行管理,有一天你需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。这个和我们在应用开发中比较类似:如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。
所以Vuex也使用了单一状态树来管理应用层级的全部状态。
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。
State的使用:
储存数据类似data
在vueX上的使用:
代码示例:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
那我们如何在页面去获取我们的State的值呢?
代码示例:
this.$store.todos[0]
有时候,我们需要从store中获取一些state变异后的状态,并且进行计算的时候就可以使用Getters
Getters可以对数据进行一些运算,但不能改变state的值,类似 computed。
Getters的使用:
代码示例:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})
那我们如何在页面去获取我们的getters的值呢?
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值
代码示例:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Vuex的store状态的更新唯一方式:提交Mutation
Mutation主要包括两部分:
字符串的事件类型(type)一个回调函数(handler),该回调函数的第一个参数就是state。
mutation的定义方式:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
那我们如何在页面中通过触发mutation来来改变state状态呢?
通过mutation更新
store.commit('increment')
但是如果参数不是一个呢?
比如我们有很多参数需要传递,这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象.
这个时候可以再从对象中取出相关的信息.
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
页面调用:
store.commit('increment', {
amount: 10
})
上面的通过commit进行提交是一种普通的方式,Vue还提供了另外一种风格, 它是一个包含type属性的对象
VueX定义:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
页面调用:
store.commit({
type: 'increment',
amount: 10
})
Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.
这就要求我们必须遵守一些Vuex对应的规则:
例子:
当我们点击更新信息时, 界面并没有发生对应改变
查看下面代码的方式一和方式二,都可以让state中的属性是响应式的.
通常情况下, Vuex要求我们Mutation中的方法必须是同步方法.
主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照.
但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.
比如我们之前的代码, 当执行更新时, devtools中会有如下信息: 图1
但是, 如果Vuex中的代码, 我们使用了异步函数: 图2
你会发现state中的info数据一直没有被改变, 因为他无法追踪到.
So, 通常情况下, 不要再mutation中进行异步的操作
我们强调, 不要再Mutation中进行异步操作,但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.
Action的基本使用代码如下:
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
context是什么?
context是和store对象具有相同方法和属性的对象,也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等。但是注意, 这里它们并不是同一个对象, 为什么呢? 我们后面学习Modules的时候, 再具体说.
这样的代码是否多此一举呢?
我们定义了actions, 然后又在actions中去进行commit, 这不是脱裤放屁吗?
事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了.
在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch,
store.dispatch('increment')
Module是模块的意思, 为什么在Vuex中我们要使用模块呢?
Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理。当应用变得非常复杂时,store对象就有可能变得相当臃肿.为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutations、actions、getters等
我们按照什么样的方式来组织模块呢?
我们来看代码
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
上面的代码中, 我们已经有了整体的组织结构, 下面我们来看看具体的局部模块中的代码如何书写.
我们在moduleA中添加state、mutations、getters、mutation和getters接收的第一个参数是局部状态对象
注意:虽然, 我们的doubleCount和increment都是定义在对象内部的.但是在调用的时候, 依然是通过this.$store来直接调用的.
actions的写法呢? 接收一个context参数对象
局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
如果getters中也需要使用全局的状态, 可以接受更多的参数