Vuex的使用

什么是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的相应规则,例如:

  1. 像vue中使用data一样,在state对象中提前初始化好属性
  2. 为属性对象添加新属性时,使用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中查找相关函数
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享