Vuex
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
什么情况下我应该使用 Vuex?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式简单状态管理起步使用)就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。
Vuex是vue的状态管理工具,为了更方便的实现多个组件共享状态。
(以上内容来自官网)
基本使用
安装
npm install vuex --save
使用
- 引入vuex
import Vuex from 'vuex';
- 使用vuex
Vue.use(Vuex);
- 创建store实例
const store = new Vuex.Store({
state: {
count: 0
}
})
- 将store挂载上
new Vue({
store,
})
State
state
故名思意是状态,其实就是vuex存放数据的地方,我们可以将多个组件需要共同的使用的变量放在state
中,state
是一个对象。
使用state
我们将变量放在state
中,那么如何使用呢?
Vuex
通过store
选项,提供了一种机制将状态从跟组件“注入”到每一个子组件中(调用Vue.use(Vuex)
)。通过在根实例中注册store
选项,该store
实例会注入到根组件下的所有子组件中,且子组件能通过this.$store
访问。像这样:
<div>
喜欢人数: {{ $store.state.count }}
</div>
由于 Vuex 的状态存储是响应式的,从 store
实例中读取状态最简单的方法就是在计算属性中返回某个状态:
computed: {
count () {
return this.$store.state.count
}
}
mapState 辅助函数
当一个组件需要获取多个状态时,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用mapState
辅助函数帮助我们生成计算属性:
import { mapState } from 'vuex';
computed: {
...mapState(['count']),
}
此时就有一个计算属性count
,我们就可以直接使用它了,也可以为它重新起一个别名:
computed: {
...mapState({
storeCount: state => state.count,
})
}
如果仅仅只是返回一个state中的一个数据,我们也可以简写:
computed: {
...mapState({
storeCount: 'count', // 等同于 state => state.count
})
}
Getter
有时候我们可以能需要从state
中派生一下状态,比如说,我们需要对count
进行双倍处理,我们就可以使用getter
,我们可以把getter
认为是store
中的计算属性。getter
的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter
接收state
作为其第一个参数,Getter
也可以接受其他 getter
作为第二个参数
getters: {
countDouble (state) {
return state.count * 2;
}
}
通过属性访问
Getter
会暴露为store.getters
对象:this.$store.getters.countDouble
<div>
喜欢人数的两倍: {{ $store.getters.countDouble }}
</div>
通过方法访问
也可以让getter
返回一个函数,来实现给getter
传参
getters: {
countAdd: state => num => state.count + num;
/*
countAdd:function(state){
return function(num){
return state.count + num
}
}
*/
}
this.$store.countAdd(3);
mapGetters 辅助函数
与mapState
相似,我们也可以通过mapGetters
将store
种的getters
映射到计算属性种
import { mapsGetters } from 'vuex';
computed: {
...mapGetters([
'countDouble',
'countAdd',
])
}
如果你想将一个 getter
属性另取一个名字,使用对象形式:
mapGetters({
storeCountDouble: 'countDouble'
})
Mutation
在严格模式下,更改 Vuex
的 store
中的状态的唯一方法是提交 mutation
。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
在组件中提交 Mutation
但是我们不能直接调用mutation,当我们想要使用时,我们可以条用store.commit
方法来触发一个mutation handler
。像这样:
this.$store.commit('increment');
mapMutations 辅助函数
当然我们也可以使用辅助函数,例如:
import { mapMutations } from 'vuex'
methods: {
...mapMutations([
'increment',
]),
...mapMutations({ //同样的起一个别名
add: 'increment'
})
}
辅助函数会将this.increment()
或this.add()
映射为this.$store.commit('increment')
提交载荷(Payload)
你可以向store.commit
传入额外的参数,官方的学术名称叫载荷(payload):
mutations: {
increment (state, num) {
state.count += num
}
}
this.$store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的mutation
会更易读:
mutations: {
increment (state, payload) {
state.count += payload.num
}
}
this.$store.commit('increment', {
num: 10
})
对象风格的提交方式
提交 mutation
的另一种方式是直接使用包含 type
属性的对象:
this.$store.commit({
type: 'increment',
num: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation
函数,因此 handler
保持不变:
mutations: {
increment (state, payload) {
state.count += payload.num
}
}
使用常量替代 Mutation 事件类型
把这些常量放在单独的文件中可以让你的代码合作者对整个 app
包含的 mutation
一目了然:
// mutation-types.js
export const COUNT_INCREMENT = 'COUNT_INCREMENT'
// store.js
import Vuex from 'vuex'
import { COUNT_INCREMENT } from './mutation-types'
const store = new Vuex.Store({
state: { count:0 },
mutations: {
[COUNT_INCREMENT] (state) {
state.count++
}
}
})
用不用常量取决于自己,在需要多人协作的大型项目中,这会很有帮助。我在开发大型项目的时候,通常都会用。
Mutation 需遵守 Vue 的响应规则
下面是官网的解释:
既然 Vuex
的 store
中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue
组件也会自动更新。这也意味着 Vuex
中的 mutation
也需要与使用 Vue
一样遵守一些注意事项:
- 最好提前在你的
store
中初始化好所有所需属性。 - 当需要在对象上添加新属性时,你应该
-
使用
Vue.set(obj, 'newProp', 123)
, 或者 -
以新对象替换老对象。例如,利用对象展开运算符,我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
其实原因很简单,当我们使用对象中未有的属性时,vue
不会监听,这和vue
的知识有关,解决的方法也就是以上两种。
表单处理
在Vuex
的 state
上使用 v-model
时,由于会直接更改state
的值,所以Vue
会抛出错误。因为vuex
是不允许直接修改state
的,如果想要使用双向数据的功能,就需要自己模拟一个 v-model: :value="msg"
、@input="updateMsg"
双向绑定的计算属性
上面的做法,比v-model
本身繁琐很多,所以我们还可以使用计算属性的setter
来实现双向绑定:
<input v-model="mycount">
computed: {
mycount: {
get () {
return this.$store.state.mycount;
},
set (value) {
this.$store.commit(COUNT_INCREMENT, { num: value });
}
}
}
Mutation 必须是同步函数
要记住 mutation 必须是同步函数。
mutations: {
increment (state) {
setTimeout(() => {
state.count ++;
}, 1000)
}
}
假如,我们正在 debug
一个 app
并且观察 devtool
中的 mutation
日志,执行上端代码,我们会发现更改state的操作是在回调函数中执行的,当 mutation
触发的时候,回调函数还没有被调用,devtools
不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的
严格模式
开启严格模式,仅需在创建 store
的时候传入 strict: true
:
const store = new Vuex.Store({
// ...
strict: true
})
在严格模式下,无论何时发生了状态变更且不是由 mutation
函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
开发环境与发布环境
不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更,要确保在发布环境下关闭严格模式,以避免性能损失。我们可以通过 webpack 配合使用:
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作
Action 函数接收一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
console.log(context.state.count) //0
context.commit('increment')
}
}
})
分发Action
同样的我们在组件重调用action,需要使用 $store.dispatch
:
this.$store.dispatch('increment')
虽然和mutation
差不多,但是在action
中,可以执行异步操作,但是mutation
中不行!!!
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?我们可以使用ES6中的Promise,在项目中,通常也是会配合Promise使用
actions: {
incrementAsync ({ commit }) {//直接使用结构赋值,拿到context中的commit方法
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('increment')
resolve()
}, 1000)
})
}
}
this.$store.dispatch('incrementAsync').then(() => {
// ...
console.log("提交完成!")
})
mapActions 辅助函数
你在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment',
]),
...mapActions({
add: 'increment'
})
}
}
辅助函数会将 this.increment()
映射为 this.$store.dispatch('increment')
,将 this.add()
映射为 this.$store.dispatch('increment')
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store
对象就有可能变得相当臃肿。为了解决以上问题,Vuex
允许我们将 store
分割成模块(module
)。每个模块拥有自己的 state
、mutation
、action
、getter
。像这样:
//count.js
export const count = {
state: {
count:0
},
getters:{
countDouble(state){
return state.count * 2
}
},
mutations: {
increment(state,payload){
state.count+=payload.num
}
}
}
import {count} from './modules/count'
//...
const store = new Vuex.Store({
modules: {
count
}
})
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。但对于模块内部的 state 比较特殊,会将 module 模块中的 state 放在一个对象中,对象名就是模块的名字,在开发者工具中我们可以看到如下:
- 获取 state:state,所以也无法直接通过mapState获取state,需要加上模块名,想这样:
this.$store.state.count.count
//mapState方式
computed:{
...mapState({
count:state=>state.count.count
})
},
- 获取 getter:
this.$store.getters.countDouble
//mapGetter方式
computed:{
...mapGetters({
countDouble:"countDouble"
})
}
- 提交 mutation:
this.$store.commit('increment',{num:2});
//mapMutation方式
methods:{
...mapMutations(['increment'])
}
- 分发 action:
this.$store.dispatch('xxx');
//mapActions方式
methods:{
...mapActions方式(['xxx'])
}
命名空间
我们上面也说过了,默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,如果你想要action、mutation 和 getter 同样是局部的话,可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。
//count.js
export const count = {
//...
namespaced: true,
}
- 获取 state:和没用命名空间相同
this.$store.state.count.count
//mapState方式
computed:{
...mapState({
count:state=>state.count.count
})
}
- 获取 getter:
this.$store.getters['count/doubleCount']
this.$store.getters['count/countDouble']
//mapGetter方式
computed:{
...mapGetters({
countDouble:"count/countDouble"
})
}
- 提交 mutation:
this.$store.commit('moduleName/xxx')
this.$store.commit("count/increment",{num:2})
//mapMutation方式
methods:{
...mapMutations({
add:'count/increment'
})
}
在使用的时候,最好像上面例子中一样为mutaions
起一个别名,否则在使用的时候就是这样调用的了this[count/increment]
- 分发 action:
this.$store.dispatch('moduleName/xxx')
this.$store.dispatch("count/xxx")
//mapActions方式
methods:{
...mapActions方式({
actionCount:'count/xxx'
})
}
以上就是使用命名空间的时候使用方法,虽然我在项目中没有使用过,但感觉应该有用。
带命名空间的模块内访问全局内容
如果我们想要在命名空间的模块内访问全局内容呢?我们看一下官网上的用法,
如果你希望使用全局 state 和 getter,rootState
和 rootGetters
会作为第三和第四参数传入 getter,也会通过 context
对象的属性传入 action。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
//...
},
},
}
}
再具体用法见官网吧:https://vuex.vuejs.org/zh/guide/modules.html