简单解释
简单的理解就是你在state中定义了一个数据之后,你可以在所在项目中的任何一个组件里进行获取、进行修改,并且你的修改可以得到全局的响应变更。
核心思想
Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应 的组件也会相应地得到高效更新。
- 你不能直接改变 store
中的状态。改变 store
中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具如vue-devtool帮助我们更好地了解我们的应用。另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码会将变得更结构化且易维护。
vuex工作流程
vuex文件目录
src├──
├── index.html
├── main.js
├── components
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── state.js # 根级别的 state
├── getters.js # 根级别的 getter
├── mutation-types.js # 根级别的mutations名称(官方推荐mutions方法名使用大写)
├── mutations.js # 根级别的 mutation
├── actions.js # 根级别的 action
└── modules
├── m1.js # 模块1
└── m2.js # 模块2
Vuex包含的五个基本对象
- State:单一状态树。
- Getter:计算属性。
- Mutation: 用于提交更改store中的状态(mutation是更改store中状态的唯一方法)。
- Action:用于提交mutation,可以包含异步操作。
- Module:当应用程序很大时,需要管理的状态很多时,需要将state进行拆分,分割成模块(modules),最后统一管理。
项目安装与引入
首先在项目中安装vuex
npm install vuex --save
注意:这里一定要加上–save因为这个包我们在生产环境中也要使用。
然后 在src文件目录下新建一个名为store的文件夹,为方便引入并在store文件夹里新建一个index.js,里面的内容如下:
import Vue from "vue";
import Vuex from "vuex";
import state from "./state";
import mutations from "./mutations";
import getters from "./getters";
import actions from "./actions";
import userinfo from "./module/userinfo";
Vue.use(Vuex);
const store = new Vuex.Store({
state, // state:state 的简写
getters,
mutations,
actions,
modules: {
userinfo
}
});
export default store;
接下来,在 main.js里面引入store,然后再全局注入一下,这样一来就可以在任何一个组件里面使用this.$store了:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store/index"; // 引入store
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
说了上面的前奏之后,接下来就是纳入正题了,就是开篇说的state的玩法。
State
简单理解:在 store 中的 state 对象,可以理解为 Vue 实例中的 data 对象,它用来保存最基本的数据。
声明
export default {
nums: 0, // 数量
price: 100, // 金额
count: 0, // 总计
obj: {} // 定义一个空对象
}
在Vue中获取store中的状态:
<template>
<div class="hello">
<div>{{price}}</div>
</div>
</template>
<script>
import store from '../store/index.js'; // 1. 对象字面量方法需引入store
export default {
computed: {
// 1. 对象字面量方法获取
// price () {
// return store.state.price
// },
// 2.通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。
price () {
return this.$store.state.price
}
}
}
</script>
实际上做完上面的步骤你已经可以用this.$store.state.price在任何一个组件里面获取price定义的值了,但这不是理想的获取方式;当数据多时这种方法明显效率过低,所以 Vuex 中提供了 mapState 方法用于批量映射 store 中的状态。
MapState映射
<template>
<div class="hello">
<div>{{a}}和{{b}}</div>
</div>
</template>
<script>
import { mapState} from 'vuex';
export default {
computed: {
// 3.辅助函数
...mapState({
a: state => state.nums,
b: 'price'
}),
}
}
</script>
上例中a. 可以通过 ES6 中的箭头函数进行数据的映射,b. 当计算属性的名称与 state 的属性名一致时可能直接通过字符串赋值。
如果所有计算属性的名称都与 state 一致,可以在 mapState 中以数组的方式进行映射。如果 Vue 中已经存在计算属性,可以通过 ES6 的对象展开运算符 (…) 进行组合。
<template>
<div class="hello">
<div>{{nums}}和{{price}}</div>
</div>
</template>
<script>
import { mapState} from 'vuex';
export default {
computed: {
// 3.辅助函数
...mapState(['nums', 'price'])
}
}
</script>
在 Vuex 模块化中,state 是唯一会根据组合时模块的别名来添加层级的,后面的 getters、mutations 以及 actions 都是直接合并在 store 下**。**
Mutations
简单理解:在vuex中,更改state 的方式只有提交mutation.大家可以把他就想象成vue中methods 中的一个方法。
状态提交
// mutation.js
import * as types from './mutation-types'
export default {
[types.SET_NUMS] (state, nums) {
// 必写state
state.nums = nums
},
[types.SET_PRICE] (state, price) {
state.price = price
},
// 改变state状态
increments (state) {
state.count = state.count + 1
},
// 提交载荷
incrementsTwo (state, payload) {
state.count += payload
},
// 提交载荷
incrementsThree (state, payload) {
state.count = state.count + payload.num1
},
// 对象风格的传参
incrementsFour (state, payload) {
state.count = state.count + payload.num1 + payload.num2
}
}
// HelloWorld.vue文件
<template>
<div class="hello">
<div @click="increments">mutation提交</div>
<div>mutation提交的值:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
methods() {
increments () {
this.$store.commit('increments')
}
}
}
</script>
想要改变状态的时候都是用this.$store.commit的方式
传参方式
每一个 mutation 都有一个字符串的事件类型和一个回调函数,每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
第一种方式:提交载荷(Payload)
你可以向 this.$store.commit 传入额外的参数,即 mutation 的 载荷(payload):
// HelloWorld.vue文件
<template>
<div class="hello">
<div @click="incrementsTwo">mutation提交载荷按钮</div>
<div>mutation提交载荷的状态:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
methods() {
incrementsTwo () {
this.$store.commit('incrementsTwo', 10)
}
}
}
</script>
官方推荐,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// xxx.vue文件
<template>
<div class="hello">
<div @click="incrementsThree">mutation对象方式提交载荷按钮</div>
<div>mutation对象方式提交载荷的状态:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
methods() {
incrementsThree () {
this.$store.commit('incrementsThree', {num1: 30})
}
}
}
</script>
**第二种方式:对象风格的传参方式
**提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
// xxx.vue文件
<template>
<div class="hello">
<div @click="incrementsFour">4)mutation----对象风格传参按钮</div>
<div>mutation对象风格传参状态:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
methods() {
incrementsFour () {
this.$store.commit({
type: 'incrementsFour', // 事件名
num1: 30, // 参数1
num2: 20 // 参数2
})
}
}
}
</script>
Mutation遵守Vue的响应原则
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
- 使用
Vue.set(obj, 'newProp', 123)
, 或者 - 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:
- 使用
state.obj = { ...state.obj, newProp: 123 }
例如:
//mutation.js文件
export default {
// { ...state.obj, ...payload } 是指以新对象替换老对象
changeNum1 (state, payload) {
state.obj = { ...state.obj, ...payload }
}
}
// HelloWorld.vue文件
<template>
<div class="hello">
<button @click="changeName">对象新增属性按钮</button>
<div>对象新增属性测试:{{name}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
data () {
return {
name: { // 定义name对象
a: 'aaaa'
}
}
},
methods() {
// 5.以新对象替换老对象
changeName () {
// this.name.b = 'bbbb' // 这样新增属性是错误的
this.$set(this.name, 'b', 'bbbb') // 当需要在对象上添加新属性时,你应该 Vue.set(obj, 'xxx', xx)
// this.name = { ...this.name, b: 'bbbb' } // 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写
}
}
}
</script>
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SET_NUMS = 'SET_NUMS' // 数量
export const SET_PRICE = 'SET_PRICE' // 加个
export const SET_FIRSTNAME = 'SET_FIRSTNAME' // firstname
export const SET_LASTNAME = 'SET_LASTNAME' // lastname
//mutations.js
import * as types from './mutation-types'
export default {
// 使用常量替代 Mutation 事件类型
[types.SET_NUMS] (state, nums) {
// 必写state
state.nums = nums
},
// 使用常量替代 Mutation 事件类型
[types.SET_PRICE] (state, price) {
state.price = price
}
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="add">add事件</button>
<div href="#">add事件操作后的nums值:{{a}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
data () {
return {
name: { // 定义name对象
a: 'aaaa'
}
}
},
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
},
methods() {
add () {
let nums = this.a
nums++
this.setNums(nums)
},
...mapMutations({
setNums: type.SET_NUMS,
setprice: type.SET_PRICE
})
}
}
</script>
Mutation 必须是同步函数
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用 —— 实质上任何在回调函数中进行的的状态的改变都是不可追踪的。
在组件中提交mutation
你可以在组件中使用 this.$store.commit(‘xxx’) 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)
// mutation-types.js
export const SET_NUMS = 'SET_NUMS' // 数量
export const SET_PRICE = 'SET_PRICE' // 加个
export const SET_FIRSTNAME = 'SET_FIRSTNAME' // firstname
export const SET_LASTNAME = 'SET_LASTNAME' // lastname
//mutations.js
import * as types from './mutation-types'
export default {
// 使用常量替代 Mutation 事件类型
[types.SET_NUMS] (state, nums) {
// 必写state
state.nums = nums
},
// 使用常量替代 Mutation 事件类型
[types.SET_PRICE] (state, price) {
state.price = price
}
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="add">add事件</button>
<div href="#">add事件操作后的nums值:{{a}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
data () {
return {
name: { // 定义name对象
a: 'aaaa'
}
}
},
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
},
methods() {
add () {
let nums = this.a
nums++
this.setNums(nums)
},
// mapMutations辅助函数
...mapMutations({
setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
})
}
}
</script>
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:接下来聊聊actions
Actions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
action注册
// state.js
export default {
nums: 0, // 数量
price: 100, // 金额
count: 0, // 总计
obj: {} // 定义一个空对象
}
// mutations
export default {
incrementsFour (state, payload) {
state.count = state.count + payload.num1 + payload.num2
}
}
// actions.js
import * as types from './mutation-types'
export default {
// 注册一个简单的action
increment ({ commit }, payload) {
commit({
type: 'incrementsFour',
num1: payload.num1,
num2: payload.num2
})
},
}
分发Action
Action 通过 this.$store.dispatch方法触发:
// state.js
export default {
nums: 0, // 数量
price: 100, // 金额
count: 0, // 总计
obj: {} // 定义一个空对象
}
// mutations
export default {
incrementsFour (state, payload) {
state.count = state.count + payload.num1 + payload.num2
}
}
// actions.js
import * as types from './mutation-types'
export default {
// 注册一个简单的action
increment ({ commit }, payload) {
commit({
type: 'incrementsFour',
num1: payload.num1,
num2: payload.num2
})
},
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="useAction">action调用</button>
<div>被action调用后的值:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations} from 'vuex';
export default {
data () {
return {
name: { // 定义name对象
a: 'aaaa'
}
}
},
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
},
methods() {
useAction () {
// action的dispatch分发
this.$store.dispatch('increment', { num1: 40, num2: 30 })
}
}
</script>
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
increment ({ commit }, payload) {
setTimeout(() => {
commit({
type: 'incrementsFive',
num1: payload.num1,
num2: payload.num2
})
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
useAction () {
// 1.action的dispatch分发 action支持以载荷形式分发
// this.$store.dispatch('increment', { num1: 40, num2: 30 })
// 2.action的dispatch分发 action支持以对象形式分发
this.$store.dispatch({ type: 'increment', num1: 40, num2: 30 })
}
在组件中分发Action
你在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
):
// HelloWorld.vue
<template>
<div class="hello">
<button @click="useAction">action调用</button>
<div>被action调用后的值:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
},
methods() {
useAction () {
this.changeIncrement()
},
// mapAction辅助函数
...mapActions({
changeIncrement: 'increment' // 将 `this.changeIncrement()` 映射为 `this.$store.dispatch('increment')`
}),
}
</script>
组合Action
我们如何才能组合多个 action,以处理更加复杂的异步流程?
// action.js
import * as types from './mutation-types';
export default {
changeNumAndPrice ({ commit }) {
// 自定义触发mutations里函数的方法,context与store 实例具有相同方法和属性
commit(types.SET_NUMS, 100)
commit(types.SET_PRICE, 100)
},
// 注册一个简单的action
increment ({ commit }, payload) {
commit({
type: 'incrementsFive',
num1: payload.num1,
num2: payload.num2
})
},
actionB (context) {
console.log('失败')
},
// 分组action
actionA ({ dispatch }, payload) {
return dispatch('increment', {
num1: payload.num1,
num2: payload.num2
}).then(() => {
return dispatch('actionB')
})
}
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="useAction">action调用</button>
<div>被action调用后的值:{{count}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
},
methods() {
useAction () {
this.changeIncrement({ num1: 20, num2: 30 })
},
// mapAction辅助函数
...mapActions({
changeIncrement: 'actionA'
}),
}
</script>
有一点要注意的是,将 store 中的 state 绑定到 Vue 组件中的 computed 计算属性后,对 state 进行更改需要通过 mutation 或者 action,在 Vue 组件中直接进行赋值 (this.myState = ‘ABC’) 是不会生效的。
Getters
有时候我们需要根据store 中的 state 来派生出一些其他状态,例如对列表进行过滤并计数,再比如:依据商品单价和商品数量计算商品总价。这时候可以使用getters计算属性:
Getters获取
// getters.js
export default {
total: state => { // 商品总价
return state.nums * state.price
}
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="add">add事件</button>
<div>getters获取的值:{{total}}</div>
<button @click="changePrice">改变价格</button>
<div>{{total}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions} from 'vuex';
export default {
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
// 1.获取getters
total () {
return this.$store.getters.total
}
},
methods() {
add () {
let nums = 1
nums++
this.setNums(nums)
},
// changePrice
changePrice () {
let nums = 3
nums++
this.setprice(nums)
},
// 6. mapMutations辅助函数
...mapMutations({
setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
}),
}
</script>
辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
// getters.js
export default {
total: state => { // 商品总价
return state.nums * state.price
}
}
// HelloWorld.vue
<template>
<div class="hello">
<button @click="add">add事件</button>
<div>getters获取的值:{{total}}</div>
<button @click="changePrice">改变价格</button>
<div>{{total}}</div>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions,mapGetters} from 'vuex';
export default {
computed: {
...mapState({
a: state => state.nums, //获取nums状态
b: 'price',
count: 'count'
}),
// 2.mapGetters辅助函数
...mapGetters(['total'])
},
methods() {
add () {
let nums = this.a
nums++
this.setNums(nums)
},
// changePrice
changePrice () {
let price = this.price
price++
this.setprice(price)
},
// 6. mapMutations辅助函数
...mapMutations({
setNums: type.SET_NUMS, // 将 `this.setNums()` 映射为 `this.$store.commit('setNums')`
setprice: type.SET_PRICE // 将 `this.setprice()` 映射为 `this.$store.setprice('setNums')`
}),
}
</script>
Module
因为在大多数的项目中,我们对于全局状态的管理并不仅仅只有一种情况的需求,有时有多方面的需求,比如写一个商城项目,你所用到的全局state可能是关于购物车这一块儿的也有可能是关于用户信息这一块儿的;像这样的情况我们就要考虑使用vuex中的 modules 模块化了,具体怎么使用modules呢?请往下看
首先,在store文件夹下面新建一个modules文件夹,然后在modules文件里面建立需要管理状态的js文件,既然要把不同部分的状态分开管理,那就要把它们给分成独立的状态文件了,如下图:
而对应的store文件夹下面的index.js 里面的内容就直接改写成:
// store文件下index.js
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import getters from './getters'
import mutations from './mutations'
import actions from './actions'
import userinfo from './module/userinfo'
Vue.use(Vuex)
const store = new Vuex.Store({
state,
getters,
mutations,
actions,
modules: {
userinfo
}
})
export default store
命名空间:namespaced: true, 为什么要加这个呢?默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的。比如说:你在全局的action.js下定义了一个方法,然后在某个模块下定义了相同的方法就会产生覆盖,加命名空间的目的是为了在某个模块下定义的方法加上模块名,与全局的方法区分开。
// module文件下的userinfo.js
import * as types from '../mutation-types'
export default {
namespaced: true, // 命名空间
state: {
firstName: '--',
lastName: '--'
},
getters: {
fullName (state) {
return state.firstName + state.lastName
}
},
mutations: {
[types.SET_FIRSTNAME] (state, payload) {
state.firstName = payload
},
[types.SET_LASTNAME] (state, payload) {
state.lastName = payload
}
},
actions: {
changeName ({ commit }) {
// 注意: 想要访问跟级别的状态可以使用rootState
setTimeout(() => {
commit(types.SET_FIRSTNAME, 'ling')
commit(types.SET_LASTNAME, 'xi')
}, 1000)
}
}
}
// HelloWorld.vue调用
<template>
<div class="hello">
<h1>获取userinfo模块的状态:firstName: {{firstName}} lastName: {{lastName}}</h1>
</div>
</template>
<script>
import * as type from '../store/mutation-types'
import { mapState,mapMutations,mapActions,mapGetters} from 'vuex';
export default {
computed: {
...mapState('userinfo', ['firstName', 'lastName']),
},
methods() {
...mapActions('userinfo', ['changeName'])
},
created () { // 调用userinfo的changeName方法
this.changeName()
}
}
</script>
最后
好了,本文到此结束,希望对你有帮助 😃
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。