Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
为什么需要Vuex?
在 Vue 应用中,通常使用单向数据流,父组件将状态传递给子组件,这样可以很好地管理数据流动。然而,当遇到多个组件需要共享状态时,这种简单的单向数据流变得难以维护。具体问题如下:
- 多个视图依赖同一状态:
- 当多个组件依赖于相同的状态时,通过父组件传参会非常繁琐,特别是组件嵌套层次较深时,需要层层传递数据。
- 对于兄弟组件间的状态传递,单向数据流也无法直接满足需求,因为状态通常只能在父组件和子组件之间传递,而兄弟组件没有直接的共享机制。
- 来自不同视图的行为需要变更同一状态:
- 在复杂应用中,不同视图可能会触发同一状态的更改。常见的做法是让父子组件直接引用状态或通过事件触发同步状态,但这样会导致状态的多份拷贝,增加了状态管理的复杂性。
- 如果不小心处理,容易出现不同组件的状态不一致,导致维护成本增加,代码变得难以理解和维护。
Vuex的优势
为了解决以上问题,Vuex 提供了一种全局状态管理的方式,将共享状态抽取出来,用一个全局单例来管理。这种模式下:
- 组件树可以作为一个“巨大的视图”:任意组件都可以访问或改变全局状态,不再受限于层层传递数据。
- 状态与视图分离:Vuex 通过定义严格的状态管理规则,保持状态与视图的独立性,避免了直接操作状态带来的不可维护性。
通过 Vuex,应用的代码结构更加清晰,状态流动也更具规律性,因此在组件间共享状态时更易维护和扩展。
第一个Vuex实例
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state){
state.count++
}
}
})
//触发state变更和获取state
state.commit('increment')
console.log(store.state.count) // 输出1
- 响应式状态存储:
- Vuex 的状态存储是响应式的。当 Vue 组件从 Vuex 的
store中读取状态时,如果该状态发生了变化,Vuex 会自动通知相关的组件进行高效更新。这种响应性在组件中使用全局变量时无法实现,因为普通的全局变量并不会触发视图的自动更新。
- Vuex 的状态存储是响应式的。当 Vue 组件从 Vuex 的
- 状态更改受控:
- 在 Vuex 中,不能直接修改状态。修改状态的唯一方法是通过提交 (
commit)mutation,这提供了一个严格的状态变更管理流程。通过mutation的集中处理,可以方便地跟踪所有状态的变化记录,为调试和维护带来了极大的便利。而普通的全局变量可以在任何地方任意修改,导致状态的变化不可追踪,增加了维护难度。
- 在 Vuex 中,不能直接修改状态。修改状态的唯一方法是通过提交 (
State
由于Vuex使用单一状态树,单个对象包含了应用的全部层级状态,所以每个应用仅仅包含一个store
如果有些状态严格属于单个组件,最好还是作为组件的局部状态
在Vue组件中获取state
在根实例(如App.vue)中注册store并在main.js中引入,子组件可以通过this.$store来访问
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
},
handleAdd(){
this.$refs.handleForm.form = {
createName: this.$store.state.thAuth.user.userInfo.userAlias,
};
},
mapState辅助函数
一个组件要获取多个state的时候,每个都在computed中写return方法有些重复。mapState函数帮助我们设置计算属性
// store.js
export const store = new Vuex.Store({
state: {
user: {
name: 'Alice',
age: 30
},
projects: ['Project A', 'Project B']
}
});
// Component.vue
<template>
<div>
<p>用户:{{ userName }}</p>
<p>年龄:{{ userAge }}</p>
<p>项目列表:</p>
<ul>
<li v-for="project in projects" :key="project">{{ project }}</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
// 使用 mapState 将 store 中的状态映射为本地计算属性
...mapState({
userName: state => state.user.name, // 映射为计算属性 userName
userAge: state => state.user.age, // 映射为计算属性 userAge
projects: state => state.projects // 映射 projects 数组
})
}
};
</script>
注:...为展开运算符,相当于手动写三个属性的return方法 当映射的计算属性的名称与 state 的子节点名称相同时,可以直接给mapState传一个字符串数组:...mapState(['user','projects'])
Getter
相当于给store设置computed属性,也是一样有缓存属性
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
//接受state为第一个参数
doneTodos: state => {
return state.todos.filter(todo => todo.done)
},
//可以接收另一个getter作为第二参数
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
},
//这里使用的是嵌套函数
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})
import { mapGetters } from 'vuex'
export default {
//通过属性获取
this.$store.getters.doneTodos//doneTodos执行结果
this.$store.getters.doneTodosCount//值为1
//通过方法获取
this.$store.getters.getTodoById(2)//这个时候不会缓存结果
computed: {
...mapGetters(
['doneTodosCount','anotherGetter']
)
//如果你想换个名字可以写成对象
...mapGetters({
doneCount: 'doneTodosCount'
})
}
}
Mutation
更改store的状态的唯一方法是提交mutation,每个mutation都有一个字符串类型的type和handler,handler接收state作为第一个参数
const store = new Vuex.Store({
state:{
count: 1
},
mutations:{
increment(state){
state.count++
}
}
})
//用相应的字符串类型type调用commit方法
store.commit('increment')
mutation的handler可以接受额外的参数payload载荷,为了确保可读性,载荷应该是一个对象
mutations: {
increment(state, payload){
state.count += payload.amount
}
}
//提交时传入payload
store.commit('increment',{
amount: 10
})
//也可以直接提交一个有type的payload
store.commit({
type: 'increment',
amount: 10
})
由于 Vuex 的 store 状态是响应式的,因此任何状态变更都会触发监视该状态的 Vue 组件自动更新。但为了确保这种响应式更新能够正常工作,mutation 在操作状态时需要遵循一些 Vue 的响应式规则:
提前在state初始化所有要用到的属性(比如上文中的state.count)
动态添加新属性,比如使用Vue.set或者对象展开运算符
const mutations = {
setUserAddress(state, address){
//要在state.user上添加新属性,有两种方法
Vue.set(state.user, 'address', address)
//使用展开符
state.user = { ...state.user, address: address }
}
}
在组件中可以使用mapMutations来映射,相当于把this.$store.commit()映射到组件里的方法
export default {
methods: {
//把this.$store.commit('increment')映射到this.incrementCount方法上
...mapMutations({
incrementCount: 'increment',
decrementCount: 'decrement'
})
//如果两个方法名字不改,可以用字符串数组
...mapMutations(['increment', 'decrement'])
}
}
在mutation中使用异步调用会使得程序难以调试——不知道异步方法什么时候调用,这与mutation的追踪state变化记录的设计目的背道而驰,所以mutation都是同步的
Action
但是实际开发中,异步调用的需求肯定是会遇到的,Action应运而生。Action同样提交的是mutation来修改状态,但是可以异步。
这样确保了异步操作的控制权归 action 所有,而 mutation 只负责具体的状态更新,保持同步特性。
const store = new Vuex.Store({
state:{
count: 0
},
mutations:{
increment(state){
state.count++
}
},
actions:{
increment(context){
context.commit('increment')
}
}
})
context是一个和store实例有相同方法和属性的对象,所以能通过context.commit来提交mutation,context.state和context.getters来获取
分发Action
actions:{
//用到了ES2015的参数解构,直接把context.commit获取出来,所以下面commit的时候不用写context
incrementAsnyc({ commit }){
setTimeout(() => {
commit('increment')
}, 1000)
}
}
store.dispatch('incrementAsnyc')
//和Mutation一样可以有payload
store.dispatch('incrementAsnyc',{ amount: 10 })
store.dispatch({
type: 'incrementAsync',
amount: 10
})
export default {
methods: {
//把this.$store.dispatch('increment')映射到this.incrementCount方法上
...mapActions({
incrementCount: 'increment',
decrementCount: 'decrement'
})
//如果两个方法名字不改,可以用字符串数组
...mapActions(['increment', 'decrement'])
}
}
组合Action
有的时候需要处理复杂的异步逻辑,这个时候要用到组合Action
action: {
actionA({commit}){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
commit('mutation')
resolve()
}, 1000)
})
}
}
//这个时候,你可以在分发或者另一个action中处理actionA的回调
store.dispatch('actionA').then(()=>{})
action:{
actionB({ dispatch, commit }){
//等待A完成后处理回调,在回调里提交另一个mutation
return dispatch('actionA').then(()=>{
commit('anotherMutation')
})
}
}
Module
避免store过于臃肿,可以分割为不同的模块,每个模块有自己的state,mutation,action,getter
const store = new Vuex.Store({
modules: {
app,
tagsView,
settings,
msgPop,
systemData
},
getters
})
对于模块内的action,通过context.rootState可以获取根的state