各位观众,晚上好!我是老码农,今天咱们聊聊 Vuex 里的两位“劳模”—— commit
和 dispatch
。 这俩哥们儿,一个负责“提交”,一个负责“调度”,听起来挺高大上,但其实干的都是“跑腿”的活儿。只不过跑腿的方向和方式不太一样。
咱们先从 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
}
}
从代码中可以看出:
commit
函数接收 mutation 的类型 (type) 和可选的 payload (负载)。- 它首先会检查是否存在对应类型的 mutation。 如果没有找到,会抛出一个错误。
- 如果找到了 mutation,它会调用
_withCommit
函数,该函数会设置this._committing
为true
,执行mutation,然后将this._committing
恢复原状。这确保了 mutation 的执行是同步的,并且在 mutation 执行期间,不允许其他的 mutation 被提交。 - mutation 函数会被调用,并传入 state 和 payload 作为参数。
总结一下,commit
的特点:
- 同步: mutation 必须是同步的,这意味着你不能在 mutation 里面使用
setTimeout
、Promise
等异步操作。 - 直接修改 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)
}
}
从代码中可以看出:
dispatch
函数接收 action 的类型 (type) 和可选的 payload (负载)。- 它首先会检查是否存在对应类型的 action。 如果没有找到,会抛出一个错误。
- 如果找到了 action,它会调用这个 action,并传入一个包含
commit
、dispatch
、state
、getters
等属性的 context 对象,以及 payload 作为参数。 - action 可以返回一个 Promise,这使得我们可以方便地处理异步操作的结果。
总结一下,dispatch
的特点:
- 异步: action 可以包含任意异步操作。
- 不直接修改 State: action 不直接修改 state,而是通过
commit
来提交 mutations。 - 可以包含复杂的业务逻辑: action 可以包含复杂的业务逻辑,比如多个异步操作的组合、条件判断等等。
- Action 函数接收一个 context 对象: 这个 context 对象包含了
commit
、dispatch
、state
、getters
等属性, 方便 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。
源码细节再剖析:
_committing
Flag: Vuex 使用_committing
flag 来保证 mutation 的同步性。 在commit
函数中,它会设置this._committing
为 true, 然后执行 mutation, 最后再将this._committing
恢复原状。 如果在_committing
为 true 的时候, 尝试提交 mutation, Vuex 会抛出一个错误。 这样可以避免在 mutation 内部执行异步操作, 导致状态变更不可预测。- Context 对象:
dispatch
函数在调用 action 的时候, 会传入一个 context 对象。 这个 context 对象包含了commit
、dispatch
、state
、getters
等属性。 这样 action 就可以方便地访问 store 的状态, 提交 mutation, 甚至可以 dispatch 其他的 action。 - Promise 支持: action 可以返回一个 Promise。 这样我们就可以方便地处理异步操作的结果。 在组件中, 我们可以使用
await this.$store.dispatch('actionName')
来等待 action 执行完成。
最佳实践:
- Mutation 保持简单: mutation 应该只负责修改 state 的一小部分。 避免一个 mutation 做太多的事情,这样不利于追踪状态的变化。
- Action 处理复杂的业务逻辑: action 可以包含复杂的业务逻辑,比如多个异步操作的组合、条件判断等等。
- 使用常量作为 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)
}
}
})
总结
commit
和 dispatch
是 Vuex 中非常重要的两个方法。 commit
负责同步地提交 mutation, 直接修改 state。 dispatch
负责异步地提交 action, action 可以包含复杂的业务逻辑, 最终通过 commit
来修改 state。 理解它们的区别和用法, 可以帮助我们更好地使用 Vuex 来管理应用的状态。
好了,今天的分享就到这里。 希望大家有所收获! 如果有什么疑问, 欢迎提问。