十六、状态管理——Vuex(3)

文章介绍了Vuex中处理异步操作的方式,强调mutation应保持同步,而action用于执行异步任务并提交mutation。通过示例展示了action的分发、在组件中的使用以及如何组合action来管理复杂的异步流程。同时,提供了在实际购物车应用中的例子来说明如何在action中处理商品添加到购物车的逻辑。
摘要由CSDN通过智能技术生成

本章概要

  • action
    • 分发 action
    • 在组件中分发 action
    • 组合 action

16.7 action

在定义mutation 时,一条重要的原则就是 mutation 必须是同步函数。换句话说,在 mutation() 处理器函数中,不能存在异步调用。例如:

mutations:{
  someMutation(state){
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

在 someMutation() 函数中调用 api.callAsyncMethod() 方法,传入了一个回调函数,这是一个异步调用。记住,不要这么做,因为这会让调试变得非常困难。
假设正在调试应用程序并查看 devtool 中的 mutation 日志,对于每个记录的 mutation,devtool 都需要捕捉到前一状态和后一状态的快照。然而在上面的例子中,mutation 中的 api.callAsyncMethod() 方法中的异步回调让这不可能完成。
因为当 mutation 被提交的时候,回调函数还没有被调用,devtool 也无法知道回调函数什么时候真正被调用。实质上,任何在回调函数中执行的状态的改变都是不可追踪的。

如果确实需要执行异步操作,那么应该使用 action。action 类似于 mutation,不同之处在于:

  • action 提交的是 mutation,而不是直接变更状态
  • action 可以包含任意异步操作

一个简单的 action 如下:

const store = createStore({
  state(){
    return {
      count:0
    }
  },
  mutations:{
    increment(state){
      state.count++
    }
  },
  actions:{
    increment(context){
      context.commit('increment')
    }
  }
})

action 处理函数接收一个与 store 实例具有相同方法和属性的 context 对象,因此可以利用该对象调用 commit() 方法提交 mutation,或者通过 context.state 和 context.getter 访问 state 和 getter。甚至可以用 context.dispatch() 调用其他的 action。
要注意的是,context 对象并不是 store 实例本身。
如果在 action 中需要多次调用 commit,则可以考虑使用 ES6 中的结构语法简化代码。代码如下:

actions:{
  increment({commit}){
    commit('increment')
  }
}

16.7.1 分发 action

action 通过 store.dispatch() 方法触发。如下:

store.dispatch('increment')

action 和 mutation 看似没有什么区别,实际上,他们之间最主要的区别就是 action 中可以包含异步操作。例如:

actions:{
  incrementAsync({commit}){
    setTimeout(() => {
      commit('increment')
    },1000)
  }
}

action 同样支持以载荷和对象方式进行分发。如下:

// 载荷是一个简单值
store.dispatch('incrementAsync',10)

// 载荷是一个对象
store.dispatch('incrementAsync',{
  amount:10
})

// 直接传递一个对象进行分发
store.dispatch({
  type:'incrementAsync',
  amount:10
})

一个实际的例子是购物车的结算操作,该操作涉及调用一个异步 API 和提交多个 mutation。代码如下:

actions:{
  checkout({commit,state},products){
    // 保存购物车中当前的商品项
    const savedCartItems = [...state.cart.added]
    // 发出结算请求,并乐观的清空购物车
    commit(type.CHECKOUT_REQUEST)
    // shop.buyProducts() 方法接收一个成功回调和一个失败的回调
    shop.buyProducts(
      products,
      // 处理成功
      () => commit(types.CHECKOUT_SUCCESS).
      // 处理失败
      () => commit(types.CHECKOUT_FAILURE,savedCartItems)
    )
  }
}

checkout 执行一个异步操作流,并通过提交这些操作记录 action 的副作用(状态更改)。

16.7.2 在组件中分发 action

在组件中可以使用 this.store.dispatch(‘XXX’) 方法分发 action ,或者使用 mapActions() 辅助函数将组件的方法映射为 store.dispatch 调用。如下:

store.js

const store = createStore({
  state(){
    return {
      count:0
    }
  },
  mutations:{
    increment(state){
      state.count++
    },
    incrementBy(state,n){
      state.count += n;
    }
  },
  actions:{
    increment ({commit}){
      commit('increment');
    },
    incrementBy({commit},n){
      commit('incrementBy',n)
    }
  }
})

组件

<template>
  <button @click="incrementBy(10)">
    You clicked me {{ count }} times.
  </button>
</template>
<script>
  import { mapActions } from 'vuex'
  export default{
    // ...
    methods:{
      ...mapActions([
        // 将this.increment() 映射为 this.$store.dispatch('increment')
        'increment',
        // mapActions 也支持载荷
        // 将 this.incrementBy(n) 映射为 this.$store.dispatch('incrementBy',n)
        'incrementBy'
      ]),
      ...mapActions({
        // 将 this.add() 映射为 this.$store.dispatch('increment')
        add:'increment'
      })
    }
  }
</script>

16.7.3 组合 action

action 通常是异步的,那么如何知道 action 何时完成呢?更重要的是,如何才能组合多个 action 来处理更复杂的异步流程呢?
首先,要知道是 store.dispatch() 方法可以处理被触发的 action 的处理函数返回的 Promise ,并且 store.dispatch() 方法仍旧返回 Promise。例如:

actions: {
    actionA({ commit }) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                commit('someMutation');
                resolve();
            }, 1000)
        })
    }
},

现在可以:

store.dispatch('actionA').then(() => {
  // ...
})

在另外一个 action 中也可以:

actions:{
  // ...
  actionB({ dispatch,commit }){
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果使用 async/await,则可以按以下方式组合 action。

// 假设 getData() 和 getOtherData() 返回的是 Promise
actions:{
  async actionA({commit}){
    commit('gotData',await getData())
  },
  async actionB({dispatch,commit}){
    await dispatch('actionA'); // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch() 方法在不同模块中可以触发多个 action 处理函数。在这种情况下,只有当所有触发的处理函数完成后,返回的 Promise 才会执行。
下面给出一个简单的例子,来看看如何组合 action 来处理异步流程。代码没有采用单文件组件,而是在 HTML 页面中直接编写。如下:

ComposingActions.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
	</head>
	<body>
		<div id="app">
			<book></book>
		</div>
        <script src="https://unpkg.com/vue@next"></script>
        <script src="https://unpkg.com/vuex@next"></script>
        <script>
            const app = Vue.createApp({});
        	const store = Vuex.createStore({
        	  state() {
        	    return {
        	      book: {
        	        title: '《下沉年代》', 
        	        price: 168,
        	        quantity: 1
        	      },
        	      totalPrice: 0  
        	    }
        	  },
        	  mutations: {
        	    // 增加图书数量
        	    incrementQuantity (state, quantity) {
        	      state.book.quantity += quantity;
        	    },
        	    // 计算图书总价
        	    calculateTotalPrice(state){
        	      state.totalPrice = state.book.price * state.book.quantity;
        	    }
        	  },
        	  actions: {
        	    incrementQuantity({commit}, n){
        	      // 返回一个Promise
        	      return new Promise((resolve, reject) => {
        	        // 模拟异步操作
                    setTimeout(() => {
                      // 提交mutiation
                      commit('incrementQuantity', n)
                      resolve()
                    }, 1000)
                  })
        	    },
        	    updateBook({ dispatch, commit }, n){
        	      // 调用dispatch()方法触发incrementQuantity action,
        	      // incrementQuantity action返回一个Promise,
        	      // dispatch对其进行处理,仍旧返回Promise,
        	      // 因此可以继续调用then()方法
        	      return dispatch('incrementQuantity', n).then(() => {
        	        // 提交mutation
        	        commit('calculateTotalPrice');
        	      })
        	      
        	    }
        	  }
        	})
        	
        	app.component('book', {
        	  data(){
        		return {
        		  quantity: 1
        		}
        	  },
        	  computed: {
                ...Vuex.mapState([
                  'book',
                  'totalPrice'
                ])
              },
        	  methods: {
        		...Vuex.mapActions([
        		  'updateBook',
        		]),
        		addQuantity(){
        		  this.updateBook(this.quantity)
        		}
        	  },
              template: `
                <div>
                  <p>书名:{{ book.title }}</p>
                  <p>价格:{{ book.price }}</p>
                  <p>数量:{{ book.quantity }}</p>
                  <p>总价:{{ totalPrice }}</p>
                  <p>
                    <input type="text" v-model.number="quantity">
                    <button @click="addQuantity">增加数量</button>
                  </p>
                </div>`
          });

          app.use(store).mount('#app');
        </script>
	</body>
</html>

在 state 中定义了两个状态数据:book 对象和 totalPrice ,并为修改它们的状态分别定义了 mutation:incrementQuantity 和 calculateTotalPrice,之后定义了两个 action:incrementQuantity 和 updateBook,前者模拟异步操作提交 incrementQuantity mutation 修改图书数量;后者调用 dispatch() 方法触发前者的调用,在前者成功完成后,提交 calculateTotalPrice mutation,计算图书总价。
上述页面的展示效果如下:
在这里插入图片描述

当点击“增加数量”按钮时,在 addQuantity 事件处理函数中触发的是 updateBook action ,而在该 action 方法中调用 dispatch() 方法触发 incrementQuantity action,等待后者的异步操作成功后(1s后更新了图书的数量),接着在 then() 方法中成功完成函数调用,提交 calculateTotalPrice mutation ,计算图书总价,最终在页面中渲染出图书新的数量和总价。

本例只是用于演示如何组合 action 处理异步流程,并不具有使用价值,实际开发中对于本例完成的功能不要这么做。

下面继续完善购物车程序。首先为商品加入购物车增加一个数量文本字段,并绑定到 quantity 属性上。编辑 Cart.vue ,添加的代码如下:

Cart.vue

...
<tr>
    <td>商品价格</td>
    <td><input type="text" v-model.number="price" /></td>
</tr>
<tr>
    <td>数量</td>
    <td><input type="text" v-model.number="quantity" /></td>
</tr>
...

运行项目,添加数量字段后购物车页面的显示效果如下:
在这里插入图片描述

现在我们想实现当用户输入的商品编号与现有商品的编号相同时,在购物车中不新增商品,而只是对现有商品累加数量。只有当用户添加新的商品时,才加入购物车中。这个功能的实现,就可以考虑放到 action 中去完成。
编辑 store 目录下的 index.js 文件,在 actions 选项中定义一个加入商品到购物车的 action。如下:

store/index.js

...
getters:{
    cartItemPrice(state){
        return function (id){
            let item = state.items.find(item => item.id === id);
            if(item){
                return item.price * item.count;
            }
        }
    },
    cartTotalPrice(state){
        return state.items.reduce((total,item) => {
            return total + item.price * item.count;
        },0)
    }
},
actions:{
    addItemToCart(context,book){
        let item = context.state.items.find(item => item.id === book.id);
        // 如果添加的商品已经再购物车中存在,则只增加购物车中商品的数量
        if(item){
            context.commit('incrementItemCount',book);
        }
        // 如果添加的商品是新商品,则加入购物车中
        else{
            context.commit('pushItemToCart',book);
        }
    }
}
...

需要注意的是,在action 中不要直接修改状态,而应该通过提交 mutation 更改状态。
接下来编辑 Cart.vue ,通过分发 addItemToCart 这个 action 来实现商品加入购物车的完整功能。如下:

Cart.vue

import { mapMutations, mapState, mapGetters,mapActions } from 'vuex';
methods: {
    ...mapMutations({
        addItemToCart: 'pushItemToCart',
        increment: 'incrementItemCount'
    }),
    ...mapMutations([
        'deleteItem'
    ]),
    ...mapActions([
        'addItemToCart'
    ]),
    addCart() {
        // this.$store.commit('pushItemToCart', {
        this.addItemToCart({
            id: this.id,
            title: this.title,
            price: this.price,
            count: this.quantity
        })
        this.id = '';
        this.title = '';
        this.price = '';
    },
}

此时添加新商品和现有商品,测试一下购物车程序的运行情况。

提醒:
在本例中,简单起见,对是否是现有商品仅通过商品编号来判断,所以在添加商品时,只要编号相同就认为是同一种商品。而在实际项目中不会这么做。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值