解释 Vuex 源码中 `commit` 和 `dispatch` 方法的实现,以及它们在触发 `mutations` 和 `actions` 时的区别。

各位观众,晚上好!我是老码农,今天咱们聊聊 Vuex 里的两位“劳模”—— commitdispatch。 这俩哥们儿,一个负责“提交”,一个负责“调度”,听起来挺高大上,但其实干的都是“跑腿”的活儿。只不过跑腿的方向和方式不太一样。

咱们先从 Vuex 的基本结构说起,这样才能理解它们俩到底跑的是哪条腿。

Vuex 的“四梁八柱”

Vuex 就像一个数据集中营,把应用的状态(state)集中管理起来,然后通过一些特定的方式来修改这些状态。 它的核心概念有四个:

  • State (状态): 存放着应用的数据,相当于一个全局的 “变量池”。
  • Mutations (变更): 唯一修改 State 的方式,必须是同步的。 就像一个“盖章处”,只有它才能在你的数据上盖章生效。
  • Actions (动作): 提交 Mutations 的地方,可以包含异步操作。 可以理解为“办事大厅”,你可以在这里提交申请,但真正盖章的还是 Mutations。
  • Getters (获取器): 从 State 中派生出一些计算状态,类似于 Vue 的 computed 属性。

commit:同步盖章,一步到位

commit 方法负责提交 mutations。 它的作用很简单粗暴: 找到对应的 mutation,然后执行它,修改 state。

咱们先来一段代码,看看 commit 是怎么使用的:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    },
    decrement (state) {
      state.count--
    }
  },
  actions: {
    incrementAsync ({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
})

export default store
// MyComponent.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { mapState, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapActions(['incrementAsync']),
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
      this.$store.commit('decrement')
    }
  }
}
</script>

在这个例子中,我们在 MyComponent.vue 中通过 this.$store.commit('increment') 来提交一个名为 increment 的 mutation。 commit 会在 store 中找到 increment mutation,然后执行它,将 state.count 加 1。

现在,咱们深入源码,看看 commit 内部是怎么实现的。简化后的核心代码如下(这里只关注核心逻辑,省略了插件和模块相关的代码):

// Vuex 源码 (简化版)

class Store {
  constructor (options = {}) {
    this._committing = false // 用于限制mutation必须同步执行

    // mutations
    this._mutations = Object.create(null)
    const mutations = options.mutations || {}
    for (const type in mutations) {
      this._mutations[type] = (payload) => {
        mutations[type].call(this, this.state, payload) // 执行mutation
      }
    }

     // actions
    this._actions = Object.create(null)
    const actions = options.actions || {}
    for (const type in actions) {
      this._actions[type] = (payload) => {
        return actions[type].call(this, {
          commit: this.commit,
          dispatch: this.dispatch,
          state: this.state,
          getters: this.getters,
          rootState: this.state,
          rootGetters: this.getters
        }, payload)
      }
    }
    this.commit = (type, payload, _options) => {
        const mutation = this._mutations[type]

        if (!mutation) {
          console.error(`[vuex] unknown mutation type: ${type}`)
          return
        }

        this._withCommit(() => { // 限制mutation必须同步执行
          mutation(payload)
        })
      }

    this.dispatch = (type, payload) => {
      const action = this._actions[type]

      if (!action) {
        console.error(`[vuex] unknown action type: ${type}`)
        return
      }

      return action(payload)
    }
  }

  _withCommit (fn) { // 保证mutation同步执行的辅助函数
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

从代码中可以看出:

  1. commit 函数接收 mutation 的类型 (type) 和可选的 payload (负载)。
  2. 它首先会检查是否存在对应类型的 mutation。 如果没有找到,会抛出一个错误。
  3. 如果找到了 mutation,它会调用 _withCommit 函数,该函数会设置 this._committingtrue,执行mutation,然后将 this._committing 恢复原状。这确保了 mutation 的执行是同步的,并且在 mutation 执行期间,不允许其他的 mutation 被提交。
  4. mutation 函数会被调用,并传入 state 和 payload 作为参数。

总结一下,commit 的特点:

  • 同步: mutation 必须是同步的,这意味着你不能在 mutation 里面使用 setTimeoutPromise 等异步操作。
  • 直接修改 State: mutation 的作用就是直接修改 state。
  • 单一职责: 一个 mutation 应该只负责修改 state 的一小部分。 避免一个 mutation 做太多的事情,这样不利于追踪状态的变化。

dispatch:异步调度,曲线救国

dispatch 方法负责提交 actions。 Actions 可以包含任意异步操作,比如发送 Ajax 请求、定时器等等。 Actions 本身不修改 state, 而是通过 commit 来提交 mutations,最终修改 state。

在上面的代码例子中, 我们定义了一个 incrementAsync action:

// store.js
actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

这个 action 会等待 1 秒钟,然后通过 commit('increment') 来提交 increment mutation。

在组件中,我们通过 this.$store.dispatch('incrementAsync') 来触发这个 action。

再来看 dispatch 的源码 (简化版):

// Vuex 源码 (简化版)

class Store {
  // ... (省略了 constructor 和 commit)

    this.dispatch = (type, payload) => {
      const action = this._actions[type]

      if (!action) {
        console.error(`[vuex] unknown action type: ${type}`)
        return
      }

      return action(payload)
    }
}

从代码中可以看出:

  1. dispatch 函数接收 action 的类型 (type) 和可选的 payload (负载)。
  2. 它首先会检查是否存在对应类型的 action。 如果没有找到,会抛出一个错误。
  3. 如果找到了 action,它会调用这个 action,并传入一个包含 commitdispatchstategetters 等属性的 context 对象,以及 payload 作为参数。
  4. action 可以返回一个 Promise,这使得我们可以方便地处理异步操作的结果。

总结一下,dispatch 的特点:

  • 异步: action 可以包含任意异步操作。
  • 不直接修改 State: action 不直接修改 state,而是通过 commit 来提交 mutations。
  • 可以包含复杂的业务逻辑: action 可以包含复杂的业务逻辑,比如多个异步操作的组合、条件判断等等。
  • Action 函数接收一个 context 对象: 这个 context 对象包含了 commitdispatchstategetters 等属性, 方便 action 与 store 进行交互。

commit vs dispatch: 异同点大 PK

特性 commit dispatch
类型 同步 异步
修改 State 直接修改 通过 commit 间接修改
作用 提交 mutation 提交 action
适用场景 简单的状态变更 复杂的业务逻辑、异步操作
参数 mutation 类型 (type), payload action 类型 (type), payload
返回值 无返回值 可以返回 Promise
目的 变更state 最终也会变更state,但执行过程更复杂,可以包含异步逻辑

举个例子:

假设我们要实现一个“购买商品”的功能。

  • mutation (同步): SET_CART_ITEMS: 修改 state 中的购物车商品列表。
  • action (异步): checkout: 发送 Ajax 请求到服务器,提交订单,然后提交 SET_CART_ITEMS mutation,清空购物车。

代码如下:

// store.js
const store = new Vuex.Store({
  state: {
    cartItems: []
  },
  mutations: {
    SET_CART_ITEMS (state, items) {
      state.cartItems = items
    }
  },
  actions: {
    async checkout ({ commit, state }) {
      try {
        // 模拟发送 Ajax 请求
        const response = await fakeApi.submitOrder(state.cartItems)

        if (response.success) {
          commit('SET_CART_ITEMS', []) // 清空购物车
          alert('订单提交成功!')
        } else {
          alert('订单提交失败!')
        }
      } catch (error) {
        console.error('订单提交出错:', error)
        alert('订单提交出错!')
      }
    }
  }
})

// 模拟 API
const fakeApi = {
  submitOrder (items) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        // 模拟成功或失败
        const success = Math.random() > 0.5
        if (success) {
          resolve({ success: true })
        } else {
          resolve({ success: false })
        }
      }, 500)
    })
  }
}

在这个例子中,checkout action 负责处理异步的“提交订单”操作,最终通过 commit 来修改 state。

源码细节再剖析:

  1. _committing Flag: Vuex 使用 _committing flag 来保证 mutation 的同步性。 在 commit 函数中,它会设置 this._committing 为 true, 然后执行 mutation, 最后再将 this._committing 恢复原状。 如果在 _committing 为 true 的时候, 尝试提交 mutation, Vuex 会抛出一个错误。 这样可以避免在 mutation 内部执行异步操作, 导致状态变更不可预测。
  2. Context 对象: dispatch 函数在调用 action 的时候, 会传入一个 context 对象。 这个 context 对象包含了 commitdispatchstategetters 等属性。 这样 action 就可以方便地访问 store 的状态, 提交 mutation, 甚至可以 dispatch 其他的 action。
  3. Promise 支持: action 可以返回一个 Promise。 这样我们就可以方便地处理异步操作的结果。 在组件中, 我们可以使用 await this.$store.dispatch('actionName') 来等待 action 执行完成。

最佳实践:

  1. Mutation 保持简单: mutation 应该只负责修改 state 的一小部分。 避免一个 mutation 做太多的事情,这样不利于追踪状态的变化。
  2. Action 处理复杂的业务逻辑: action 可以包含复杂的业务逻辑,比如多个异步操作的组合、条件判断等等。
  3. 使用常量作为 mutation 和 action 的类型: 这样可以避免拼写错误, 提高代码的可维护性。
// mutation-types.js
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'

// action-types.js
export const INCREMENT_ASYNC = 'INCREMENT_ASYNC'

// store.js
import * as types from './mutation-types'
import * as actionTypes from './action-types'

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    [types.INCREMENT] (state) {
      state.count++
    },
    [types.DECREMENT] (state) {
      state.count--
    }
  },
  actions: {
    [actionTypes.INCREMENT_ASYNC] ({ commit }) {
      setTimeout(() => {
        commit(types.INCREMENT)
      }, 1000)
    }
  }
})

总结

commitdispatch 是 Vuex 中非常重要的两个方法。 commit 负责同步地提交 mutation, 直接修改 state。 dispatch 负责异步地提交 action, action 可以包含复杂的业务逻辑, 最终通过 commit 来修改 state。 理解它们的区别和用法, 可以帮助我们更好地使用 Vuex 来管理应用的状态。

好了,今天的分享就到这里。 希望大家有所收获! 如果有什么疑问, 欢迎提问。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注