Vuex actions 和 Flux 中的 "action creators" 是等同的概念,但是我觉得这个定义常让人感到困惑(比如分不清 actions 和 action creators)。
Actions 是用于分发 mutations 的函数。按照惯例,Vuex actions 的第一个参数是 store 实例,附加上可选的自定义参数。
// 最简单的 action
function increment (store) {
store.dispatch('INCREMENT')
}
// 带附加参数的 action
// 使用 ES2015 参数解构
function incrementBy ({ dispatch }, amount) {
dispatch('INCREMENT', amount)
}
乍一眼看上去感觉多此一举,我们直接分发 mutations 岂不更方便?实际上并非如此,还记得 mutations 必须同步执行这个限制么?Actions 就不受约束!我们可以在 action 内部执行异步操作:
function incrementAsync ({ dispatch }) {
setTimeout(() => {
dispatch('INCREMENT')
}, 1000)
}
来看一个更加实际的购物车示例,涉及到调用异步 API 和 分发多重 mutations:
function checkout ({ dispatch, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出检出请求,然后乐观地清空购物车
dispatch(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => dispatch(types.CHECKOUT_SUCCESS),
// 失败操作
() => dispatch(types.CHECKOUT_FAILURE, savedCartItems)
)
}
请谨记一点,必须通过分发 mutations 来处理调用异步 API 的结果,而不是依赖返回值或者是传递回调来处理结果。基本原则就是:Actions 除了分发 mutations 应当尽量避免其他副作用。
你可能发现了 action 函数必须依赖 store 实例才能执行。从技术上讲,我们可以在组件的方法内部调用 action(this.$store)
来触发一个 action,但这样写起来有失优雅。更好的做法是把 action 暴露到组件的方法中,便可以直接在模板中引用它。我们可以使用 vuex.actions
选项来这么做:
// 组件内部
import { incrementBy } from './actions'
const vm = new Vue({
vuex: {
getters: { ... }, // state getters
actions: {
incrementBy // ES6 同名对象字面量缩写
}
}
})
上述代码所做的就是把原生的 incrementBy
action 绑定到组件的 store 实例中,暴露给组件一个 vm.increamentBy
实例方法。所有传递给 vm.increamentBy
的参数变量都会排列在 store 变量后面然后一起传递给原生的 action 函数,所以调用:
vm.incrementBy(1)
等价于:
incrementBy(vm.$store, 1)
虽然多写了一些代码,但是组件的模板中调用 action 更加省力了:
<button v-on:click="incrementBy(1)">increment by one</button>
还可以给 action 取别名:
// 组件内部
import { incrementBy } from './actions'
const vm = new Vue({
vuex: {
getters: { ... },
actions: {
plus: incrementBy // 取别名
}
}
})
这样 action 就会被绑定为 vm.plus
而不是 vm.increamentBy
了。
如果一个 action 只跟一个组件相关,可以采用简写语法把它定义成一行:
const vm = new Vue({
vuex: {
getters: { ... },
actions: {
plus: ({ dispatch }) => dispatch('INCREMENT')
}
}
})
如果你想简单地把所有引入的 actions 都绑定到组件中:
import * as actions from './actions'
const vm = new Vue({
vuex: {
getters: { ... },
actions // 绑定所有 actions
}
})
通常在大型 App 中,action 应该按不同目的进行 分组 / 模块化 管理,例如,userActions 模块用于处理用户注册、登录、注销,而 shoppingCartActions 处理购物任务。
当想要在不同组件中仅引入必需的 action 时,模块化使之更为方便。
你还可以在 action 模块中引入其他 action 模块来实现复用。
// errorActions.js
export const setError = ({dispatch}, error) => {
dispatch('SET_ERROR', error)
}
export const showError = ({dispatch}) => {
dispatch('SET_ERROR_VISIBLE', true)
}
export const hideError = ({dispatch}) => {
dispatch('SET_ERROR_VISIBLE', false)
}
// userActions.js
import {setError, showError} from './errorActions'
export const login = ({dispatch}, username, password) => {
if (username && password) {
doLogin(username, password).done(res => {
dispatch('SET_USERNAME', res.username)
dispatch('SET_LOGGED_IN', true)
dispatch('SET_USER_INFO', res)
}).fail(error => {
dispatch('SET_INVALID_LOGIN')
setError({dispatch}, error)
showError({dispatch})
})
}
}
当从一个模块中调用另一个模块的 action 时,或者调用同一模块中的另一个 action 时,切记,action 的第一个参数是 store 实例,因此应该将调用者 action 的第一个参数传递给被调用 action。
如果你使用 ES6 的解构形式来编写 action,确保调用者 action 的第一个参数包含两个 action 中用到的所有属性和方法。举例说明,调用者 action 仅使用 dispatch 方法,而被调用 action 使用了 state 属性和 watch 方法,那么,*dispatch*、state 和 watch 应该都出现在传递给调用者 action 的第一个形式参数中,示例如下:
import {callee} from './anotherActionModule'
export const caller = ({dispatch, state, watch}) => {
dispatch('MUTATION_1')
callee({state, watch})
}
或者,你也可以使用老式的函数语法:
import {callee} from './anotherActionModule'
export const caller = (store) => {
store.dispatch('MUTATION_1')
callee(store)
}