如何利用`Vuex`的`commit`与`dispatch`进行状态管理?

Vuex:commitdispatch的艺术

大家好!今天我们深入探讨Vuex中两个至关重要的概念:commitdispatch。它们是Vuex状态管理的核心,理解并熟练运用它们,能够帮助我们构建更清晰、更易维护的Vue应用。

Vuex,作为Vue.js的状态管理模式,采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它本质上是一个全局单例模式的状态管理器。

1. 状态(State)

在Vuex中,state是存储应用程序所有状态的地方。可以把它想象成一个全局的 data 对象。

const store = new Vuex.Store({
  state: {
    count: 0
  }
})

可以通过 store.state.count 在任何组件中访问这个状态。

2. commit: 同步修改状态的基石

commit 用于提交 mutation,是更改 Vuex store 中状态的唯一同步方法。 mutation 类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

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

现在,我们如何在组件中使用 commit 来触发这个 increment mutation呢?

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
export default {
  methods: {
    increment () {
      this.$store.commit('increment')
    }
  }
}
</script>

这里,this.$store.commit('increment') 就是触发名为 increment 的 mutation。 每次点击按钮,count 的值都会同步增加 1。

携带 Payload (载荷)

mutation 可以接受第二个参数,称为 载荷 (payload),用于传递数据。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state, payload) {
      state.count += payload
    }
  }
})

在组件中,我们这样使用:

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="increment(5)">Increment by 5</button>
  </div>
</template>

<script>
export default {
  methods: {
    increment (amount) {
      this.$store.commit('increment', amount)
    }
  }
}
</script>

现在,点击按钮,count 的值会增加 5。

使用对象风格的提交方式

除了直接传递类型字符串,我们还可以使用包含 type 属性的对象。

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="increment({ amount: 10 })">Increment by 10</button>
  </div>
</template>

<script>
export default {
  methods: {
    increment (payload) {
      this.$store.commit({
        type: 'increment',
        amount: payload.amount
      })
    }
  }
}
</script>
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state, payload) {
      state.count += payload.amount
    }
  }
})

这种方式在需要传递多个参数时更加清晰。

Mutation 必须是同步的

Vuex 的一个重要原则是:mutation 必须是同步函数。 这是因为当 mutation 触发的时候,我们可以非常容易地追踪到状态的变化。 如果 mutation 允许异步操作,那么状态的变化将变得不可预测,调试起来会非常困难。

例如,以下代码是错误的:

mutations: {
  incrementAsync (state) {
    setTimeout(() => {
      state.count++ // 错误!不要在mutation中进行异步操作
    }, 1000)
  }
}

为了处理异步操作,我们需要使用 dispatch

3. dispatch: 异步操作的指挥官

dispatch 用于触发 actionaction 可以包含任意异步操作。 action 类似于 mutation,不同之处在于:

  • action 可以包含任意异步操作。
  • action 提交的是 mutation,而不是直接修改状态。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    incrementAsync ({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

在这个例子中,incrementAsync action 在 1 秒后提交 increment mutation,从而修改 count 的值。

在组件中,我们这样使用 dispatch:

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
export default {
  methods: {
    incrementAsync () {
      this.$store.dispatch('incrementAsync')
    }
  }
}
</script>

点击按钮,1 秒后 count 的值会增加 1。

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象

action 函数接受一个 context 对象作为参数,这个对象包含:

  • state: 等同于 store.state,用来获取 state。
  • commit: 等同于 store.commit,用来提交 mutation。
  • dispatch: 等同于 store.dispatch,用来触发其他 action。
  • getters: 等同于 store.getters,用来获取 getters。
  • rootState: 可以访问根级别的 state,在模块化 store 中很有用。
  • rootGetters: 可以访问根级别的 getters,在模块化 store 中很有用。

通常,我们会使用 ES6 的解构赋值来简化代码:

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

携带 Payload (载荷)

Action 也可以接受 payload。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state, amount) {
      state.count += amount
    }
  },
  actions: {
    incrementAsync ({ commit }, amount) {
      setTimeout(() => {
        commit('increment', amount)
      }, 1000)
    }
  }
})

在组件中:

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="incrementAsync(5)">Increment Async by 5</button>
  </div>
</template>

<script>
export default {
  methods: {
    incrementAsync (amount) {
      this.$store.dispatch('incrementAsync', amount)
    }
  }
}
</script>

使用对象风格的 Dispatch

commit 类似,dispatch 也可以使用对象风格。

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <button @click="incrementAsync({ amount: 10 })">Increment Async by 10</button>
  </div>
</template>

<script>
export default {
  methods: {
    incrementAsync (payload) {
      this.$store.dispatch({
        type: 'incrementAsync',
        amount: payload.amount
      })
    }
  }
}
</script>
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state, amount) {
      state.count += amount
    }
  },
  actions: {
    incrementAsync ({ commit }, payload) {
      setTimeout(() => {
        commit('increment', payload.amount)
      }, 1000)
    }
  }
})

Action 的返回值

Action 可以返回 Promise,这使得我们可以处理异步操作的结果。

const store = new Vuex.Store({
  state: {
    todos: []
  },
  mutations: {
    setTodos (state, todos) {
      state.todos = todos
    }
  },
  actions: {
    fetchTodos ({ commit }) {
      return fetch('/api/todos') // 假设这是一个获取 todos 的 API
        .then(response => response.json())
        .then(todos => {
          commit('setTodos', todos)
        })
    }
  }
})

在组件中:

<template>
  <div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
    </ul>
    <button @click="fetchTodos">Fetch Todos</button>
  </div>
</template>

<script>
export default {
  computed: {
    todos () {
      return this.$store.state.todos
    }
  },
  methods: {
    fetchTodos () {
      this.$store.dispatch('fetchTodos')
        .then(() => {
          console.log('Todos fetched successfully!')
        })
        .catch(error => {
          console.error('Failed to fetch todos:', error)
        })
    }
  }
}
</script>

4. mapStatemapMutationsmapActions 辅助函数

为了更方便地在组件中使用 Vuex,Vuex 提供了 mapStatemapMutationsmapActions 辅助函数。 这些函数可以将 store 中的 state、mutations 和 actions 映射到组件的 computed 属性和 methods 中。

mapState

mapState 将 store 中的 state 映射到组件的 computed 属性。

import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      count: state => state.count,
      doubleCount: state => state.count * 2
    })
  }
}

现在,我们可以在模板中直接使用 countdoubleCount

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
  </div>
</template>

如果 computed 属性的名称和 state 的名称相同,可以简化写法:

import { mapState } from 'vuex'

export default {
  computed: mapState([
    'count' // 等同于 count: state => state.count
  ])
}

mapMutations

mapMutations 将 store 中的 mutations 映射到组件的 methods。

import { mapMutations } from 'vuex'

export default {
  methods: {
    ...mapMutations([
      'increment' // 将 this.increment() 映射为 this.$store.commit('increment')
    ]),
    ...mapMutations({
      add: 'increment' // 将 this.add() 映射为 this.$store.commit('increment')
    })
  }
}

现在,我们可以在模板中使用 incrementadd 方法。

<template>
  <div>
    <button @click="increment">Increment</button>
    <button @click="add">Add</button>
  </div>
</template>

mapActions

mapActions 将 store 中的 actions 映射到组件的 methods。

import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions([
      'incrementAsync' // 将 this.incrementAsync() 映射为 this.$store.dispatch('incrementAsync')
    ]),
    ...mapActions({
      asyncAdd: 'incrementAsync' // 将 this.asyncAdd() 映射为 this.$store.dispatch('incrementAsync')
    })
  }
}

现在,我们可以在模板中使用 incrementAsyncasyncAdd 方法。

<template>
  <div>
    <button @click="incrementAsync">Increment Async</button>
    <button @click="asyncAdd">Async Add</button>
  </div>
</template>

5. 模块化 Vuex Store

当应用变得复杂时,我们需要将 store 分割成模块(module)。 每个模块拥有自己的 state、mutations、actions、getters,甚至可以嵌套子模块。

const moduleA = {
  state: () => ({ count: 0 }),
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    incrementIfOdd ({ commit, state }) {
      if ((state.count + 1) % 2 === 0) {
        commit('increment')
      }
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA
  }
})

console.log(store.state.a.count) // -> 0

命名空间

默认情况下,模块内部的 action 和 mutation 仍然注册在全局命名空间——这样使得多个模块能够对同一个 action 或 mutation 作出响应。 如果希望你的模块更加独立和可复用,你可以通过添加 namespaced: true 使其成为带命名空间的模块。 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

const moduleA = {
  namespaced: true, // 开启命名空间
  state: () => ({ count: 0 }),
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    incrementIfOdd ({ commit, state }) {
      if ((state.count + 1) % 2 === 0) {
        commit('increment') // 提交的是 moduleA/increment
      }
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

在带命名空间的模块注册全局 action

如果你希望注册全局 action,可以将 root: true 添加到 action 的定义中。

actions: {
  someOtherAction ({ dispatch }) {
    dispatch('globalAction', null, { root: true })
  }
}

6. 最佳实践

  • 始终使用 mutation 修改状态。 避免直接修改 store.state,这会使状态变化不可追踪。
  • mutation 必须是同步的。 异步操作放在 action 中处理。
  • 保持 mutation 简单明了。 mutation 的职责是修改状态,不要包含复杂的业务逻辑。
  • 使用 mapStatemapMutationsmapActions 辅助函数简化代码。
  • 合理使用模块化。 将 store 分割成模块,提高代码的可维护性和可复用性。
  • 考虑使用 TypeScript。 TypeScript 可以提供类型检查,帮助你避免一些常见的 Vuex 错误。

7. commitdispatch 的区别总结

特性 commit dispatch
功能 提交 mutation 触发 action
操作类型 同步 异步
职责 直接修改 state 提交 mutation,间接修改 state
应用场景 简单的状态更新 涉及异步操作或复杂逻辑的状态更新

掌握 commitdispatch,是构建强大Vue应用的基础。记住,commit负责同步修改,dispatch负责异步调度,合理运用它们,你的Vuex代码将更加清晰易懂。

8. 进一步思考

理解了commitdispatch的用法,更重要的是理解它们背后的设计思想:数据的单向流动。通过限制状态的直接修改,并引入actions来处理复杂的逻辑和异步操作,Vuex保证了应用状态的可预测性和可维护性。 模块化可以使项目维护性更高。

发表回复

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