简介
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。拥有五个核心概念:
- State
- Getter
- Mutation
- Action
- Module
状态管理模式
对于一个简单的计数器:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `<div>{{ count }}</div>`,
// actions
methods: {
increment () {
this.count++
}
}
})
状态自管理应用包含以下几个部分:
state,驱动应用的数据源;
view,以声明方式将 state 映射到视图;
actions,响应在 view 上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
管理共享状态
当我们的应用遇到多个组件共享状态时,上面的单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
- 来自不同视图的行为需要变更同一状态。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
基于上面的思路,Vuex 为 Vue.js 专门设计了状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
项目结构
Vuex 并不限制代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
简单使用
假设上面的计数器中的count在多个页面会被用到,我们可以做如下改写:
// Count.vue
<template>
<div>
当前数值:{{$store.state.count}} //从store取count
<br>
<button @click="add">增加</button>
</div>
</template>
<script>
export default {
methods:{
add(){
this.$store.commit("addCount"); //调用store的mutations里面的addCount方法
}
},
mounted() {
console.log(this);
}
}
</script>
// store/index.js
export default new Vuex.Store({
//data
state: {
count:0
},
//method
mutations: { //推荐在mutations用method处理state的状态,而不是在直接调用处理
addCount(state){
state.count++;
}
},
//异步方法
actions: {},
//模块
modules: {}
})
打印查看$store:
State
每个应用将仅仅包含一个 store 实例,存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则。基本写法:
// store/index.js
export default new Vuex.Store({
//data
state: {
count:0,
msg:"im tom"
},
})
//xxx.vue
export default {
computed:{
count () {
return this.$store.state.count
},
msg:{
get(){ return this.$store.state.msg},
set(newValue){}
}
}
}
mapState
上面的生成步骤非常繁琐,我们可以用mapState 辅助函数简写如下:
//xxx.vue
<template>
<div>
<div>count:{{count}}</div>
<div>countAlias:{{countAlias}}</div>
<div>countPlusLocalState:{{countPlusLocalState}}</div>
</div>
</template>
<script>
//必须引入mapState
import { mapState } from 'vuex'
export default {
data() {
return {
localCount: 8
}
},
computed: mapState({
// 箭头函数可使代码更简练,相当于function(state){return state.count}
count: state => state.count,
// 传字符串参数 'msg' 等同于 `state => state.msg`
countAlias: 'msg',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState(state) {
return state.count + this.localCount
}
})
}
</script>
运行,正常展示:
上面的写法会让mapState占据computed,如果还想自己自定义对象,可以用对象展开运算符将此对象混入到外部对象中。
computed: {
result() {
return this.localCount + 5;
},
//使用了“...”展开运算符
...mapState({
// 箭头函数可使代码更简练,相当于function(state){return state.count}
count: state => state.count,
// 传字符串参数 'msg' 等同于 `state => state.msg`
countAlias: 'msg',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState(state) {
return state.count + this.localCount
}
})
}
运行,这样computed中就同时拥有了自定义的result和vuex的mapState辅助函数:
Getter
Getter 可以认为是 store 的计算属性。Getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
// store/index.js
export default new Vuex.Store({
state: {
count:0,
msg:"im tom"
},
//computed
getters:{
addCount(state){
return state.count+1;
}
},
})
//Getter.vue
<template>
<div>
{{this.$store.getters.addCount}}
</div>
</template>
运行:
Getter 也可以接受其他 getter 作为第二个参数:
// store/index.js
export default new Vuex.Store({
state: {
count:0,
msg:"im tom"
},
//computed
getters:{
addCount(state){
return state.count+1;
},
addCount2(state,getters){ //第二个参数getters
return getters.addCount + 5;
}
},
})
//Getter.vue
<template>
<div>
<div>
{{this.$store.getters.addCount}}
</div>
<div>
{{this.$store.getters.addCount2}}
</div>
</div>
</template>
运行:
通过方法访问
用该方式可以实现给getters传递参数:
// store/index.js
export default new Vuex.Store({
state: {
count:0,
},
//computed
getters:{
addCount3: (state) => (val) => {
return state.count + val;
}
},
})
//Getter.vue
<template>
<div>
<!-- 传入参数9 -->
{{this.$store.getters.addCount3(9)}}
</div>
</template>
运行:
mapGetters
Getter与 State拥有相似的辅助函数取值操作。
//Getter.vue
import {mapGetters} from 'vuex'
export default {
computed: {
//记得此时mapGetters里面是数组,区别于mapState({})
...mapGetters([
'addCount'
])
}
}
运行:
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,通过commit(“xxx”)触发。里面的方法必须是同步的,以便于它追踪store中状态的改变。
// store/index.js
export default new Vuex.Store({
state: {
count:0
},
mutations: {
addCount(state){
state.count++;
}
},
})
// xxx.vue
<script>
export default {
methods:{
add(){
this.$store.commit("addCount");
}
}
}
</script>
提交载荷
store.commit时可以传入参数,这个参数就叫mutation 的 载荷(payload)。如果要传递多个参数,vue更推荐放在一个对象里,形如:
store.commit('increment', obj);
Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
- 提前在 store 中初始化好所有所需属性,不推荐中途添加state属性。
- 如果一定要中途添加属性,要用下面方式:
//Vue.set
Vue.set(obj, 'newProp', 123)
//以新对象替换老对象
state.obj = { ...state.obj, newProp: 123 }
上面的例子只实现了state.count++,如果需要自己输入增加数值,可以这么写:
// store/index.js
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
addCount(state, obj) {
state.count += parseInt(obj.val);
}
}
})
// xxx.vue
<template>
<div>
<input type="text" v-model="obj.val" /><button @click="add">加上去吧</button>
<div>store.count:{{$store.state.count}}</div>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
val: 0
}
}
},
methods: {
add() {
this.$store.commit("addCount", this.obj);
}
}
}
</script>
运行,在Vue Devtools中可以追踪/修改vuex里面的状态变化。
mapMutations
Mutation同样支持辅助函数,可以直接在methods绑定mutations里面的函数后直接this.xxx调用,免去了commit的操作:
// store/index.js
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
addOne(state){
state.count += 1;
},
addCount(state, obj) {
state.count += parseInt(obj.val);
}
}
})
// xxx.vue
<template>
<div>
<button @click="addOne">加上1</button>
<input type="text" v-model="obj.val" />
<button @click="addCount(obj.val)">加上输入</button>
<div>store.count:{{count}}</div>
</div>
</template>
<script>
import { mapMutations,mapState } from 'vuex';
export default {
data() {
return {
obj: {
val: ''
}
}
},
computed:{
...mapState([
'count'
])
},
methods: {
logThis() {
console.log(this);
},
...mapMutations([
'addOne', //将Mutation中的addOne()作为this.addOne()
'addCount' //将Mutation中的addCount(obj)作为this.addCount(obj)
])
}
}
</script>
运行:
严格模式
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。但是不要在发布环境下启用严格模式,严格模式会深度监测状态树来检测不合规的状态变更导致程序损失性能。
//基础写法
const store = new Vuex.Store({
strict: true
})
//发布环境下 不启用 严格模式
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production'
})
表单处理
如果遇到下面这种 v-model 绑定vuex状态的,不推荐用下面这种写法,甚至在严格模式下面的写法会抛出错误。
//html
<input v-model="$store.state.message">
// store/index.js
export default new Vuex.Store({
state: {
message:0
},
})
解决方案1——单向绑定+侦听输入事件:
绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法:
//html
<input :value="message" @input="updateMessage">
// vue
computed: {
...mapState({
message: state => state.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
// store/index.js
mutations: {
updateMessage (state, message) {
state.message = message
}
}
解决方案2——计算属性的getter和setter:
//html
<input v-model="message">
// vue
computed: {
message: {
get () {
return this.$store.state.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
// store/index.js里mutations同方案1
Action
Action和Mutation一样是执行方法的,二者的不同点在于:
- Action里面可以执行异步操作,Mutation里面必须是同步的。
- Action需要通过Mutation操作state,而Mutation可以直接操作state。
- Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象作为参数,而Mutation参数是state。
- vue文件中调用Action 函数用store.dispatch,而Mutation用store.commit。
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
export default new Vuex.Store({
state: {
msg: ""
},
mutations: {
setMsg(state, txt) {
state.msg = txt;
}
},
actions: {
getTxt(context) {
axios.get(`https://api.nowtime.cc/v1/ipv4?ip=223.5.5.5`).then(res => {
let txt = res.data.result;
context.commit('setMsg',txt); //要用commit方法修改state
});
}
}
})
//xxx.vue
<template>
<div>
<button @click="getContent">拿到msg</button>
<div>{{msg}}</div>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
methods: {
getContent() {
this.$store.dispatch('getTxt'); //dispatch调用acitons
}
},
computed: {
...mapState(['msg'])
}
}
</script>
运行,这里是点击按钮后通过actions异步获取数据:
提交载荷
xxx.vue中调用Actions 用dispatch函数。Actions 支持载荷方式和对象方式进行传参:
// 载荷形式
store.dispatch('incrementAsync', {
amount: 10
})
// 对象形式
store.dispatch({
type: 'incrementAsync',
amount: 10
})
上面获取ip信息的案例,如果要修改为获取自己传入的参数作为ip信息:
// store/index.js
actions: {
getTxt(context, obj) { //传入参数
axios.get(`https://api.nowtime.cc/v1/ipv4?ip=${obj.ip}`).then(res => {
let txt = res.data.result;
context.commit('setMsg', txt);
});
}
}
//xxx.vue
methods: {
getContent() {
this.$store.dispatch('getTxt', {ip:"116.76.255.40"} ); // 载荷形式传参数
}
}
mapActions
Action同样支持辅助函数,可以直接在methods绑定Actions里面的函数后直接this.xxx调用,免去了dispatch的操作:
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
export default new Vuex.Store({
state: {
msgAli: "",
msgMe: ""
},
mutations: {
setMsgAli(state, txt) {
state.msgAli = txt;
},
setMsgMe(state, txt) {
state.msgMe = txt;
}
},
actions: {
getTxtAli(context, obj) {
axios.get(`https://api.nowtime.cc/v1/ipv4?ip=223.5.5.5`).then(res => {
let txt = res.data.result;
context.commit('setMsgAli', txt);
});
},
getTxtMe(context, ip) {
axios.get(`https://api.nowtime.cc/v1/ipv4?ip=${ip}`).then(res => {
let txt = res.data.result;
context.commit('setMsgMe', txt);
});
}
}
})
//xxx.vue
<template>
<div>
<button @click="getTxtAli">拿到阿里Ip信息</button>
<div>{{msgAli}}</div>
<button @click="getTxtMe(obj.ip)">拿到我的Ip信息</button>
<div>{{msgMe}}</div>
</div>
</template>
<script>
import { mapState,mapActions } from 'vuex';
export default {
data(){
return {
obj:{
ip : `116.76.255.40`
}
}
},
methods: {
...mapActions(['getTxtAli', 'getTxtMe']) //getTxtAli不需要参数,getTxtMe传入ip作为参数,参看store
},
computed: {
...mapState(['msgAli', 'msgMe'])
}
}
</script>
运行:
Module
如果把一个大型项目的所有状态会集中到一个比较大的对象(store)中,这个对象将非常臃肿,针对此类情况,vuex允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import Book from './book.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
},
getters: {
countMsg(state) {
return `全局count:${state.count}`;
}
},
mutations: {
addCount(state) {
++state.count;
},
reduceCount(state) {
--state.count;
},
},
actions: {
reduceCountTimeout({
state,
commit
}) {
setTimeout(function() {
commit('reduceCount');
}, 2000);
}
},
modules: { //2.放进modules
Book
}
})
定义模块时注意事项:
- mutation 和 getter里面的方法接收的第一个参数是本模块内部的state,而不是外部的。
- getters里面的方法,第三个参数是根节点状态rootState(包括自身state里的属性和modules里面的对象)。
- action的context对象包括根节点状态rootState。
下面是一个拥有book模块的modules:
//store/book.js
export default ({
state: {
bookCount: 0,
},
getters: {
//这里的rootState是 {count:0, Book:{...}}
bookCountMsg(state,getters,rootState) {
return `有${state.bookCount}本书,顺便打印rootState的count:${rootState.count}`;
}
},
mutations: {
addBookCount(state, getters) {
++state.bookCount;
},
reduceBookCount(state, getters) {
--state.bookCount;
},
},
actions: {
//延迟2秒后减1
reduceBookCountTimeout({state,commit,rootState}) {
setTimeout(function() {
commit('reduceBookCount');
}, 2000);
}
}
})
vue文件主要对store中全局与book模块数据进行展示与修改:
// vue
<template>
<div>
<div>
{{count}}<br>
{{countMsg}}
</div>
<div>
<button @click="addCount">全局+1</button>
<button @click="reduceCountTimeout">全局-1</button>
</div>
<br>
<div>
{{bookCount}}<br>
{{bookCountMsg}}
</div>
<div>
<button @click="addBookCount">Book +1</button>
<button @click="reduceBookCountTimeout">Book -1</button>
</div>
</div>
</template>
<script>
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
export default {
computed: {
...mapState(['bookCount', 'count']),
...mapGetters(['bookCountMsg', 'countMsg'])
},
methods: {
...mapMutations(['addBookCount', 'addCount']),
...mapActions(['reduceBookCountTimeout', 'reduceCountTimeout'])
}
}
</script>
运行:
同名处理
如果全局和模块的getters,mutations,actions里面有名称相同的方法呢?
//store/book.js
export default ({
state: {
bookCount: 0,
},
getters: {
msg(state,getters,rootState){
console.log("getters()");
return state.count;
}
},
mutations: {
add(state, getters){
console.log("book的mutations()");
}
},
actions: {
reduce({state,commit,rootState}){
console.log("book的reduce()");
}
}
})
//store/index.js
export default new Vuex.Store({
state: {
count: 0,
},
getters: {
msg(state,getters,rootState){
console.log("getters()");
return state.count;
}
},
mutations: {
add(state, getters){
console.log("mutations()");
}
},
actions: {
reduce({state,commit,rootState}){
console.log("reduce()");
}
},
modules: {
Book
}
})
// xxx.vue
<template>
<div>
{{msg}}
<div>
<button @click="add">Mutations</button>
<button @click="reduce">Actions</button>
</div>
</div>
</template>
<script>
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
export default {
computed: {
...mapGetters(['msg'])
},
methods: {
...mapMutations(['add']),
...mapActions(['reduce'])
}
}
</script>
运行发现执行了根目录getters但没有执行book模块的getters方法,而且控制台还报了错误,看来getters中是不允许同名的。但mutations和actions是可以同名的,只是同名会导致调用时都会被执行。
命名空间
如果希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
使用带命名空间的绑定函数
当使用 mapState, mapGetters, mapActions 和 mapMutations 这些函数来绑定带命名空间的模块时:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo',
'some/nested/module/bar'
])
}
在带命名空间的模块内访问全局内容
如果要使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 全局'someOtherGetter'
},
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 全局'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 全局'someMutation'
},
}
}
}
在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局 action,可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction') // 2.调用全局'someAction'
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // 1.在全局中注册'someAction'
}
}
}
}
}