本章概要
- 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 = '';
},
}
此时添加新商品和现有商品,测试一下购物车程序的运行情况。
提醒:
在本例中,简单起见,对是否是现有商品仅通过商品编号来判断,所以在添加商品时,只要编号相同就认为是同一种商品。而在实际项目中不会这么做。