Vuex是一个专门为Vue应用程序开发的状态管理模式+库。采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可以预测的方式发生变化。
读取vuex中的数据:this.$state.sum
修改vuex中的数据:$store.dispatch('action中的方法名',数据),或者$store.commit('mutation中的方法名',数据)
当组件发送的指令不涉及网络请求或其他的业务逻辑,组件也可以越过actions,即不写dispatch,直接commit。
创建一个简单的Store
创建需要提供一个初始state对象和一些mutation:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } } })
在创建后,现在就可以通过store.state来获取状态对象了,也能通过store.commit方法触发状态变更了:
store.commit('increment') console.log(store.state.count) // -> 1
若要在Vue组件中访问this.$store 的property,就需要为Vue实例提供创建好的store。
Vuex有一个从根组件向所有子组件,以store选项的方式"注入"该store的机制:
new Vue({ el: '#app', store: store, })
这个时候就可以从组件的方法里提交一个变更:
methods: { increment() { this.$store.commit('increment') console.log(this.$store.state.count) } }
选择通过提交mutation的方式,而非直接改变store.state.count,是因为我们想要更明确追踪到状态的变化。
核心概念
1、State(单一状态树)
用一个对象包含全部的应用层级的状态。这也就意味着,每个应用将仅仅包含一个store实例。
存储在Vuex中的数据和Vue实例中的data遵循相同规则。
那么如何在Vue的组件中来展示状态呢?
Vuex的状态存储是响应式的,那么从store实例中读取状态的最简单办法就是在计算属性里返回某个状态。
// 创建一个 Counter 组件 const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
但是,这种方式会导致组件依赖全局的状态单例。在模块化构建系统时,每个需要使用state的组件中需要频繁导入,而且在测试组件时需要模拟状态。
Vuex通过store选项,提供了一个机制从根组件"注入"到每一个子组件中(需调用Vue.use(Vuex))
const app = new Vue({ el: '#app', // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` })
这样该实例就会注入到根组件目录下的所有子组件中了,我们可以通过this.$store访问到:
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } }
对象展开运算符
因为mapState函数返回的是一个对象,我们通常需要使用一个工具函数将多个对象合并为一个,来让我们可以将最终对象传给computed属性,但是我们使用对象展开运算符可以更加简化:
computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) }
2、Getter
Vuex允许我们在store中定义"getter"(可以看做是store的计算属性)。当需要将state中的数据加工后再使用时,就需要getters来对state中的数据进行加工。当然,我们也可以在对应组件内写计算属性也可以实现,但是当计算步骤复杂时,就会导致代码的冗余。
getter在接受state作为其第一个参数:
const store = new Vuex.Store({ state: { todos: [ { id: 1, text: '...', done: true }, { id: 2, text: '...', done: false } ] }, getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } } })
通过属性访问:
Getter会暴露为store.getters对象,可以以访问属性的方式来访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter也能接受其他getter作为第二个参数:
getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } }
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } }
getter在通过属性访问时是作为Vue的响应式系统的一部分缓存在其中的。
通过方法访问
通过让getter返回一个函数,来实现给getter传参。
getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } }
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
getter在通过方法访问时,每次都会调用,而不缓存结果。
3、Mutation
更改Vuex的store库中状态的唯一方法就是提交mutation。
Vuex里的mutation类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这里的回调函数就是我们实际进行状态更改的地方,而且它会接受state作为第一个参数。
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } } })
你无法直接调用一个mutation handler
这个选项更像是事件注册:“当触发一个类型为increment的mutation的时候,调用此函数。”若要唤醒一个mutation handler,就需要以相应的type来调用store.commit方法:
store.commit('increment')
提交载荷(Payload)
可以向store.commit 传入额外参数,即mutation的载荷(payload):
// ... mutations: { increment (state, n) { state.count += n } }
store.commit('increment', 10)
大多数情况下,载荷是一个对象,这样可以包含多个字段并且记录的mutation会更加易读:
// ... mutations: { increment (state, payload) { state.count += payload.amount } }
store.commit('increment', { amount: 10 })
对象风格的提交方式
提交mutation的另一种方式是直接使用包含type属性的对象:
store.commit({ type: 'increment', amount: 10 })
当使用对象风格的提交方式时,整个对象都作为载荷传给mutation函数,所以handler保持不变:
mutations: { increment (state, payload) { state.count += payload.amount } }
Mutation需要注意遵守Vue的响应规则
由于Vuex的store中状态是响应式的,当我们变更状态的时候,监听状态 的组件也会自动更新,所以mutation也需要与使用Vue一样遵守一些注意事项:
1、最好提前在你的store中初始化好所有的所需属性。
2、当需要在对象上添加新的属性时,
应该使用Vue.set(obj,'newProp',123),或者以新对象替换老对象。例如,利用对象展开运算符这样写:
state.obj = { ...state.obj, newProp: 123 }
使用常量替代Mutation事件类型
使用常量来替代mutation事件类型在各种Flux实现很常见,因为这样可以使linter之类的工具发挥作用,同时将这些常量放在单独文件中可以让你的代码中整个app包含的mutation一目了然:
// mutation-types.js export const SOME_MUTATION = 'SOME_MUTATION'
// store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名 [SOME_MUTATION] (state) { // mutate state } } })
这在多人协作的大型项目很有用
Mutation必须要是同步函数(一条一条执行的函数)
在组件中提交Mutation
可以在组件中使用this.$store.commit('xxx')提交mutation,或者使用mapMutation辅助函数将组件中的methods映射为store.commit调用(要在根节点注入store)
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')` // `mapMutations` 也支持载荷: 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')` }) } }
4、Action
action类似于mutation
但是action提交的是mutation,不能直接变更状态;action可以包含任意异步操作,mutation都是同步事务。
注册一个简单的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.state和context.getters来获取state和getters。
在实际情况中,我们会经常用到es6的参数解构来简化代码(特别是需要调用commit很多次的时候):
actions: { increment ({ commit }) { commit('increment') } }
分发Action
Action通过store.dispatch方法触发:
store.dispatch('increment')
这里不选择直接分发mutation的原因是mutation必须同步执行,但action不受这个约束,所以我们就可以在action内部执行异步操作:
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
Action支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发 store.dispatch('incrementAsync', { amount: 10 }) // 以对象形式分发 store.dispatch({ type: 'incrementAsync', amount: 10 })
这是一个实际的购物车实例:设计了调用异步API和分发多重mutation:
actions: { checkout ({ commit, state }, products) { // 把当前购物车的物品备份起来 const savedCartItems = [...state.cart.added] // 发出结账请求,然后乐观地清空购物车 commit(types.CHECKOUT_REQUEST) // 购物 API 接受一个成功回调和一个失败回调 shop.buyProducts( products, // 成功操作 () => commit(types.CHECKOUT_SUCCESS), // 失败操作 () => commit(types.CHECKOUT_FAILURE, savedCartItems) ) } }
这里进行了一系列的异步操作,并且通过提交mutation来记录action产生的副作用(状态变更)。
在组件中分发Action
在组件中使用this.$store.dispatch('xxx')分发action,或者使用mapActions 辅助函数将组件的methods映射为store.dispatch调用(需要先在根节点注入 store
):
组合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,我们可以如下组合action:
// 假设 getData() 和 getOtherData() 返回的是 Promise actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, commit }) { await dispatch('actionA') // 等待 actionA 完成 commit('gotOtherData', await getOtherData()) } }
一个 store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。