Action 异步操作

Action 类似于 Mutation,不同之处在于:

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

基本用法

定义 Action

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

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 Mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。

参数解构

实践中,我们会经常用到 ES2015 的参数解构来简化代码:

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

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

乍一眼看上去感觉多此一举,我们为何不直接分发 Mutation 呢?实际上并非如此,还记得 Mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 Action 内部执行异步操作:

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

Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

在组件中分发 Action

使用 this.$store

export default {
  methods: {
    increment() {
      this.$store.dispatch('increment')
    }
  }
}

使用 mapActions 辅助函数

import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions([
      'increment',
      'incrementBy'
    ]),
    ...mapActions({
      add: 'increment'
    })
  }
}

组合 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

如果我们利用 async / await,可以如下组合 Action:

actions: {
  async actionA({ commit }) {
    commit('gotData', await getData())
  },
  async actionB({ dispatch, commit }) {
    await dispatch('actionA')
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 Action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

实际应用示例

用户登录

const store = new Vuex.Store({
  state: {
    user: null,
    token: null,
    loading: false,
    error: null
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user
    },
    SET_TOKEN(state, token) {
      state.token = token
    },
    SET_LOADING(state, loading) {
      state.loading = loading
    },
    SET_ERROR(state, error) {
      state.error = error
    }
  },
  actions: {
    async login({ commit }, credentials) {
      commit('SET_LOADING', true)
      commit('SET_ERROR', null)
      
      try {
        const response = await api.login(credentials)
        commit('SET_USER', response.user)
        commit('SET_TOKEN', response.token)
        localStorage.setItem('token', response.token)
        return response
      } catch (error) {
        commit('SET_ERROR', error.message)
        throw error
      } finally {
        commit('SET_LOADING', false)
      }
    },
    
    async logout({ commit }) {
      try {
        await api.logout()
      } finally {
        commit('SET_USER', null)
        commit('SET_TOKEN', null)
        localStorage.removeItem('token')
      }
    }
  }
})

获取数据列表

actions: {
  async fetchPosts({ commit, state }, { page = 1 } = {}) {
    if (state.postsLoading) return
    
    commit('SET_POSTS_LOADING', true)
    
    try {
      const response = await api.getPosts({ page })
      commit('SET_POSTS', response.data)
      commit('SET_PAGINATION', response.pagination)
    } catch (error) {
      commit('SET_POSTS_ERROR', error.message)
    } finally {
      commit('SET_POSTS_LOADING', false)
    }
  }
}

购物车操作

actions: {
  async addToCart({ commit, state }, product) {
    const existingItem = state.cart.find(item => item.id === product.id)
    
    if (existingItem) {
      commit('UPDATE_CART_ITEM_QUANTITY', {
        id: product.id,
        quantity: existingItem.quantity + 1
      })
    } else {
      commit('ADD_CART_ITEM', {
        ...product,
        quantity: 1
      })
    }
    
    try {
      await api.syncCart(state.cart)
    } catch (error) {
      console.error('同步购物车失败', error)
    }
  }
}

Action 与 Mutation 的区别

特性MutationAction
作用直接修改状态提交 Mutation
异步不支持支持
调用方式commitdispatch
调试容易追踪需要额外处理

最佳实践

统一命名规范

export const ACTION_TYPES = {
  FETCH_USER: 'fetchUser',
  FETCH_POSTS: 'fetchPosts',
  CREATE_POST: 'createPost',
  UPDATE_POST: 'updatePost',
  DELETE_POST: 'deletePost'
}

错误处理

actions: {
  async fetchData({ commit }) {
    commit('SET_LOADING', true)
    
    try {
      const data = await api.fetchData()
      commit('SET_DATA', data)
      return { success: true, data }
    } catch (error) {
      commit('SET_ERROR', error.message)
      return { success: false, error: error.message }
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

防抖处理

let searchTimer = null

actions: {
  searchProducts({ commit }, keyword) {
    if (searchTimer) {
      clearTimeout(searchTimer)
    }
    
    searchTimer = setTimeout(async () => {
      commit('SET_SEARCH_LOADING', true)
      try {
        const results = await api.searchProducts(keyword)
        commit('SET_SEARCH_RESULTS', results)
      } finally {
        commit('SET_SEARCH_LOADING', false)
      }
    }, 300)
  }
}

取消请求

let pendingRequest = null

actions: {
  async fetchUsers({ commit }, params) {
    if (pendingRequest) {
      pendingRequest.cancel('请求已取消')
    }
    
    const cancelToken = axios.CancelToken.source()
    pendingRequest = cancelToken
    
    try {
      const response = await api.getUsers(params, {
        cancelToken: cancelToken.token
      })
      commit('SET_USERS', response.data)
    } catch (error) {
      if (!axios.isCancel(error)) {
        commit('SET_ERROR', error.message)
      }
    } finally {
      pendingRequest = null
    }
  }
}