什么是Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
安装vuex
// 使用npm或yarn安装
npm i vuex
yarn add vuex
复制代码
使用Vuex
先看看一个简单的Vuex实例如何使用
import Vue from 'vue'
import Vuex from 'vuex'
const store = new Vuex.Store({
// 定义state,所有状态存储的对象
state: {
count: 0
},
// 更改state的唯一方式,就是提交mutation
mutations: {
plusOne(state) {
console.log(state);
if (state?.count === 0) {
state.count++;
} else {
state *= 2;
}
}
}
});
// 两秒后,提交plusOne的动作,并且打印state对象,预期state.count应该会+1
setTimeout(()=>{
store.commit("plusOne");
console.log(store.state);
},2000)
// state是响应式的,如果在组件中使用则会更新对应视图
// 从简单的demo中就可以得知vuex的用处,类似于一个全局的window对象,所以状态都存在state中,通过其他实例方法来获取,修改,以及拆分解耦模块。
复制代码
state
state定义是一个对象,vuex中的state和vue中的data遵循相同的规则
获取state
因为state是响应式的,最简单的方式便是通过计算属性返回数据,前提是在根组件new Vue时,构造选项中传入store对象,子组件便可以通过this.$store进行方法state
// 在main.js中
const store = new Vuex.Store({
state:{
age:10
}
})
const app = new Vue({
store,
render: h => h(App),
})
app.$mount('#app')
// 子组件中
export default {
computed:{
getInfo(){
// 返回state中存储的年龄
return this.$store.state.age
}
}
}
复制代码
mapState辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:
- mapState函数返回的是一个对象,函数接收对象或字符串数组,对象中属性的值有函数和字符串两种写法,函数则接收state为第一个参数(如果需要局部组件自身状态, 则不能写箭头函数),字符串则传入state对象的属性名。
- 字符串数组传入的是state中需要映射的属性名
//子组件
import { mapState } from "vuex";
// 也可以不解构,引入Vuex,使用Vuex.mapState
// mapState使用对象
export default {
data(){
return {
testStr:'这是测试'
}
},
computed: mapState({
stateInfo: (state) => state.age, // 使用箭头函数,this为undefined
stateAlias: "age", // 使用字符串,等价于箭头函数写法
stateUseThis(state){ // 使用常规函数
// 这里可以通过this访问组件实例
return this.testStr + state.age // 返回字符串“这是测试10”
}
})
}
// mapState使用字符串数组
export default {
// 前提是计算属性的名字和对象属性名相同,此时可以使用this.age访问state.age
computed: mapState(["age"])
}
复制代码
局部计算属性与mapState混用
使用展开运算符...
将mapState返回值展开(mapState的代码以传入对象举例,传入字符串数组同理),或者使用Object.assign进行融合对象,只要最终给computed属性返回一个对象即可
//子组件
import { mapState } from "vuex";
// 也可以不解构,引入Vuex,使用Vuex.mapState
// 使用展开运算符
export default {
computed: {
test() {
return "这是测试";
},
...mapState({
stateInfo: (state) => state.age, // 使用箭头函数,this为undefined
stateAlias: "age", // 使用字符串
stateUseThis(state) {
// 使用常规函数
// 这里可以通过this访问组件实例
return this.testStr + state.age; // 返回字符串“这是测试10”
},
}),
}
}
// 使用Object.assign
export default {
computed: Object.assign(
{
test() {
return "test";
},
},
mapState({
stateInfo: (state) => state.age,
stateAlias: "age",
stateUseThis(state) {
return this.testStr + state.age;
},
})
)
}
复制代码
getters
getters的定义是个对象,与vue的computed作用和特性类似,返回值会根据依赖被缓存,只有依赖值发生改变才会重新计算,当成store的computed用即可。getter会暴露为store实例对象的getters属性,可以通过属性访问对应的值。
const store = new Vuex.Store({
state: {
name: 'mike',
age: 30,
children: [
{ name: 'ben', age: 1, gender: 'male' },
{ name: 'susan', age: 2, gender: 'female' },
]
},
// getter接受state作用第一个参数
getters: {
getBoys: state => state.children.filter(child => child.gender === 'male')
// 返回children数组中,所有性别为男性的对象
}
})
// 组件中访问
export default {
created(){
console.log(this.$store.getters.getBoys)
// 输出getBoys的返回值
}
}
复制代码
getter除了接受state外,还接受整个getters作为第二个参数
const store = new Vuex.Store({
state: {
name: 'mike',
age: 30,
privateChildCount: 10, // 私生子数量
children: [
{ name: 'ben', age: 1, gender: 'male' },
{ name: 'susan', age: 2, gender: 'female' },
]
},
getters: {
getBoys: (state) => {
return state.children.filter(child => child.gender === 'male')
},
getTotalChildCount: (state, getters) => {
// 假设男孩才是自己亲生的,则通过getters.getBoys获取男孩的数量,再加上已知的私生子数量10个,可得出亲生孩子的总数
return state.privateChildCount + getters.getBoys.length
}
}
})
复制代码
通过方法访问,让getter返回一个函数,来实现传递参数,对于state中的数组查询操作较为使用,区别是getter通过方法访问时,不会缓存结果,每次都是执行调用。
const store = new Vuex.Store({
state: {
name: 'mike',
age: 10,
privateChildCount: 10,
children: [
{ name: 'ben', age: 1, gender: 'male' },
{ name: 'susan', age: 2, gender: 'female' },
]
},
getters: {
// findChildByAge返回一个函数,函数接收age来进行数据的筛选
findChildByAge: state => age => state.children.filter(child => child.age === age)
}
})
// 组件中调用
export default {
created(){
// 会返回并打印出state中,children数组中所有age为2的对象数组
console.log(this.$store.getters.findChildByAge(2));
}
}
复制代码
mapGetters 辅助函数
与mapState类似,将映射store中的getters到computed中,接收参数可以是字符串数组,也可以是对象,与其他局部计算属性混合使用参考mapState
// 接收一个对象,给getter重命名
export default {
created(){
console.log(this.myGetter(2));
},
computed: mapGetters({
// 将findChildByAge重命名为myGetter
myGetter: "findChildByAge",
}),
}
// 接收字符串
export default {
created(){
console.log(this.findChildByAge(2));
},
// 此时可以使用this.findChildByAge访问store.getters.findChildByAge
computed: mapGetters(['findChildByAge']),
}
复制代码
mutations
mutations的定义是个对象,修改store状态的唯一方式便是提交mutation。mutation类似于一个事件,都有事件的名称(字符串)和回调函数,回调函数是要进行更改状态的地方,它接收state作为第一个参数。
const store = newe Vuex({
state:{
age:0
},
mutations:{
addAge(state){
state.age++
}
}
})
// 把mutations当成事件注册,而不是直接去调用回调函数,想要执行mutation,需要使用store.commit([传入mutation的名称])
store.commit("addAge")
// 此时会执行名称为addAge的函数,state已+1
复制代码
可以在提交commit时提交额外的Payload,也就是参数,定义mutation时,接收payload。通常会将payload传入一个对象(也可以是其他数据类型),可包含多个字段并便于代码阅读
const store = newe Vuex({
state:{
age:0
},
mutations:{
// payload是个number,更改state.age为state.age+payload
// addAge(state,payload){
// state.age += payload
// }
payload是对象
addAge(state,payload){
state.age += payload.age
}
}
})
// 传入对象
store.commit("addAge",{age:10})
// 传入number
//store.commit("addAge",10)
复制代码
对象风格的提交方式,需要包含type属性
当使用对象风格时,整个对象都会作为payload传递给mutation函数,按照上面代码例子,更改为对象风格不需要修改代码
// mutation
addAge(state,payload){
// payload会带上type属性
state.age += payload.age
}
// commit对象风格提交
store.commit({type:'addAge',age:10})
复制代码
mutation需要遵循Vue的相应规则,例如:
- 像vue中使用data一样,在state对象中提前初始化好属性
- 为属性对象添加新属性时,使用Vue.set进行数据劫持监听,或者…和Object.assign赋值一个新对象
必须要要注意的是,mutation必须是同步函数,否则debug工具无法抓捕快照
mapMutations
将mutation映射到vue实例的methods中,使用方式与mapGetters一致,接受对象或字符串数组,并返回一个对象
const store = new Vuex.Store({
mutations:{
test1(){},
test2(payload){console.log(payload)}
}
})
// 子组件中
import { mapMutations } from 'vuex';
export default {
methods:{
// 使用字符串数组
...mapMutations(["test1","test2"]),
// 使用对象,并进行mutation重命名
...mapMutations({ myAction:'test1' })
},
// 现在子组件中,即可通过this.方法名来提交mutation,无需显示的传入mutation名称,如果有payload,放在this.方法名(payload)的payload中
created(){
this.test(); // 提交名为test1的mutation,等价this.$sotre.commit('test')
this.test2('test') // 提交名为test2的mutation,并传入payload
this.myAction() // 等价于this.test
}
}
复制代码
actions
actions的定义是个对象,actions类似于mutations,actions提交的是mutations,而不是直接更改state,且可以包含异步操作,action函数接受一个与store具有相同属性方法的对象,可以从对象中发起commit,获取state和getters。
const store = new Vuex.Store({
state: {
age: 10
},
mutations: {
addAge: (state, payload) => {
state.age += payload.age
}
},
actions: {
addAge(context) {
// context拥有和store相同属性方法
// 在action中,没有同步限制,可以执行异步操作
setTimeout(() => {
context.commit('addAge', { age: 20 })
}, 2000)
}
// 常用解构方法从参数中解构commit方法
// addAge({ commit }) {
// commit('addAge', { age: 20 })
// }
}
})
// 子组件中
export default {
created(){
// action需要通过dispatch进行分发
this.$store.dispatch("addAge")
}
}
复制代码
action支持payload和对象方式进行分发,使用方式与mutation一致,参考即可
模拟一个购物车案例来体验action的使用
// 模拟一个接口是结账的,细节忽略,主要是为了展现异步
const api = {
shop: {
buyProducts() {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve({
result: null,
success: true,
error: null
})
} catch (error) {
reject(error)
}
}, 1000)
})
}
}
}
// actions中发起异步行为,并进行提交mutation来记录状态变更。
const store = new Vuex.Store({
state: {
// 当前购物车数据
cartList: [
{ productName: "苹果", count: 20, price: 2 },
{ productName: "西瓜", count: 12, price: 15 },
{ productName: "榴莲", count: 3, price: 80 },
]
},
mutations: {
// 清空购物车
cleanUp(state) {
state.cartList = []
},
// 恢复购物车
checkoutFaild(state, cartList) {
alert('结算失败')
state.cartList = cartList
console.log("购物车数据已恢复");
console.log(state.cartList);
}
},
actions: {
// 结账
checkout({ state, commit }) {
// 将购物车进行备份
const tempCartList = [...state.cartList]
// 提交一个mutation清空购物车
commit('cleanUp')
// 假设发起了一个异步请求,1s后会返回数据
api.shop.buyProducts().then(res => {
// 忽略支付等流程,如果成功则弹出购物完成消息
if (res.success) alert('结算成功,购物已完成')
}, () => {
// 如果失败,则恢复购物车
commit('checkoutFaild', tempCartList)
})
}
}
})
// 组件中进行分发action
export default {
created(){
this.$store.dispatch('checkout')
// demo代码是写死的,所以分发action后的预期是弹出结算成功提示
}
}
// 验证恢复购物车,只需要在模拟api的测试代码中try代码块中抛出异常,就会提交恢复购物车的mutation
// 其他代码忽略
setTimeout(() => {
try {
// 在这里抛出异常
throw new Error('我是故意抛错的')
} catch (error) {
reject(error)
}
}, 1000)
// 改完再运行,便会弹出失败提示,并提交mutation恢复购物车数据
复制代码
mapActions
将actions映射到vue实例的methods中,使用方式与mapGetters一致,接受对象或字符串数组,并返回一个对象
const store = new Vuex.Store({
actions:{
test1(){},
test2(payload){console.log(payload)}
}
})
// 子组件中
import { mapActions } from 'vuex';
export default {
methods:{
// 使用字符串数组
...mapActions(["test1","test2"]),
// 使用对象,并进行action重命名
...mapActions({ myAction:'test1' })
},
// 现在子组件中,即可通过this.方法名来分发action,无需显示的传入action名称,如果有payload,放在this.方法名(payload)的payload中
created(){
this.test(); // 分发名为test1的action,等价this.$sotre.dispatch('test')
this.test2('test') // 分发名为test2的action,并传入payload
this.myAction() // 等价于this.test
}
}
复制代码
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise。
// 在action处理函数中,返回一个Promise,使用上述购物车例子进行改写
const store = new Vuex.Store({
//state等省略
actions: {
// 结账action
checkout({ state, commit }) {
return new Promise((resolve, reject) => {
const tempCartList = [...state.cartList]
commit('cleanUp')
api.shop.buyProducts().then(res => {
// 忽略支付等流程,如果成功则弹出购物完成消息
if (res.success) alert('结算成功,购物已完成')
resolve(true)
}, () => {
// 如果失败,则恢复购物车
commit('checkoutFaild', tempCartList)
reject(false)
})
})
},
// 在其他action中也可以分发action
testAction({ dispatch }){
dispatch('checkout').then(
(flag) => console.log(flag),
(flag) => console.log(flag)
);
}
}
})
// 在组件中使用
export default {
created(){
this.$store.dispatch('checkout').then(=>{
// 此时action处理函数已执行完成,并resolve或reject都会返回一个布尔值
(flag) => console.log(flag),
(flag) => console.log(flag)
})
}
}
复制代码
modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
// 将本来传入new Vuex.Store的对象,改为传入对象的modules对象属性中
const moduleA = {
state:{
id : 0
}
}
const moduleB = {
state:{
id : 1
}
}
const store = new Vuex.Store({
modules:{
moduleA,
moduleB
}
})
// 此时moduleA和moduleB都属于store实例的state对象的属性
// 获取moduleA的state的id
console.log(store.state.moduleA.id)
复制代码
模块的局部状态
对于模块内部的getter和mutation,接受的第一个参数是局部状态对象,也就是state是模块自己的state,使用方式不变
对于模块内部的action,局部状态通过context.state暴露,而根节点的状态通过context.rootState暴漏
对于模块内部的getter,根节点通过第三个参数暴漏
模块的命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
但是可以通过给模块添加属性 namespaced:true的方式,使其成为带命名空间的模块。当模块被注册后,它的所有getter,action,mutation都会自动根据模块注册的路径调整命名。
const moduleA = {
namespaced:true, // 添加属性
state:{ // state不受影响
id : 0
},
getters:{
getId(state){ // 在外部调用时需要使用store.getters.moduleA/getId
return state.id.toString()
}
},
mutations:{
// 在外部调用时需要使用store.commit('moduleA/plusId')
plusId(state){
state.id++;
console.log(state.id)
}
},
actions:{
// 在外部调用时需要使用store.dispatch('moduleA/asyncPlusId')
asyncPlusId({ commit }){
// 启用命名空间后,action中接收的getters/dispatch/commit都是局部化的,所以调用不需要加命名前缀
setTimeout(()=>{
commit('plusId') // 调用带命名的mutation
},2000)
}
}
}
const store = new Vuex.Store({
modules:{
moduleA
}
})
// 在组件中使用
export default {
created(){
// 获取命名空间的getter
console.log(this.$sotre.getters['moduleA/getId'])
// 提交一个mutation,commit需要传入[模块名/mutation名称]
this.$store.commit('moduleA/plusId')
// 分发一个action,dispatch需要传入[模块名/action名称]
this.$store.dispatch('moduleA/asyncPlusId')
}
}
复制代码
命名模块中再嵌套modules,会自动继承父模块的命名空间,以访问命名模块的命名子模块getter举例,则需要store.getters[‘父模块名/子模块名/getter名’],子模块不是命名模块则不需要加
在命名空间中访问全局内容
如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
const moduleA = {
namespaced:true, // 添加属性
state:{ // state不受影响
id : 0
},
getters:{
getId(state,getters,rootState,rootGetters){
// getters是局部的getters,只包含模块自身及子模块的getter
// rootGetters则是全局的getters
// 局部getter中,访问子命名模块的getter,需要使用[子模块名/子模块getter名]
return state.id.toString()
}
},
mutations:{
// 在外部调用时需要使用store.commit('moduleA/plusId')
plusId(state){
state.id++;
console.log(state.id)
}
},
actions:{
// 在外部调用时需要使用store.dispatch('moduleA/asyncPlusId')
asyncPlusId({ commit }){
// 启用命名空间后,action中接收的getters/dispatch/commit都是局部化的,所以调用不需要加命名前缀
setTimeout(()=>{
commit('plusId') // 调用带命名的mutation
},2000)
}
}
}
复制代码
在带命名空间的模块注册全局 action
const store = new Vuex.Store({
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction') // 分发命名模块下注册的全局action
}
},
modules: {
modulesA: {
namespaced: true,
actions: {
globalAction: {
// 改成对象形式,并传入root:true,将action的操作写在handler函数中,handler函mapState数的第一个参数接收的是当前命名模块
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
// 此action便成了全局的action,无需加命名前缀
}
}
}
})
复制代码
带命名空间的绑定函数
使用mapState,mapGetters,mapMutations,mapActions映射时,使用原来的写法会比较繁琐
computed: {
...mapState({
aId: state => state.modulesA.id,
bId: state => state.modulesA.id
})
},
methods: {
...mapActions([
"moduleB/asyncPlusId" // -> this["moduleB/asyncPlusId"]()
])
}
复制代码
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为
computed: {
...mapState("moduleB", {
bId: (state) => state.id,
}),
},
methods: {
...mapActions("moduleB", ["asyncPlusId"]) // -> this.asyncPlusId()
},
复制代码
而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions , mapMutations , mapGetters } = createNamespacedHelpers('moduleB')
// 后续的map系列函数使用时,便会在moduleB中查找相关函数
复制代码