Appearance
VueX
学习目标
- 【了解】VueX基本介绍
- 【掌握】VueX核心概念-State
- 【掌握】VueX核心概念-Getter
- 【掌握】VueX核心概念-Action
- 【掌握】VueX核心概念-Mutation
- 【了解】VueX核心概念-Module
相信大家学习到这里对于如何使用Vue来构建应用已经有了一定的认识,在复杂的应用中,数据通信是开发者最需要关注的事情,之前已经学习了很多组件间传递数据的方式,接下来要给大家介绍的VueX也是状态管理方案的一种,而且通过学习大家会马上喜欢上它。
1. 基本介绍
这里介绍的VueX是匹配Vue3的V4版本,它绝大多数功能使用都和之前基本保持一致。
1.1 官方定义
先一起看一下官网对于VueX的定义:
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1.2 状态管理
官网定义中提到了一个现代前端中非常重要的概念状态管理 ,怎么来理解这个概念呢?
- 在Vue中我们会定义一些驱动应用的响应式数据,我们把这些数据叫做状态(state)
- 定义的初始化数据会按照Vue的模板语法以声明式的方式映射到视图(view)
- 用户在视图上的操作(actions)会引起状态变化从而导致视图刷新
在复杂的应用中,我们会经常遇到以下的问题:
- 多个视图依赖于同一状态。
- 来自不同视图的操作需要变更同一状态。
其实这些问题我们也能解决,但需要状态在组件之间反复传递,十分容易出现错误,开发和维护成本都非常高,我们为什么不把组件的共享状态抽取出来,放到一个单独的空间管理呢,这个空间可以被任意组件访问到,这其实就是VueX的设计思想!下面这幅图等到学完VueX后回看将会变得非常容易理解。
1.3 VueX的必要性
并不是所有的项目中都需要使用VueX,如果你不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果你的应用够简单,你最好不要使用 Vuex。但是如果你需要构建一个中大型应用,在组件外部管理状态往往是一个更好的选择,那么这个时候VueX便会是自然而然的选择了。
1.4 项目安装
通过npm命令在项目中安装VueX
npm i vuex --save
在package.json中确认正确的版本号
1.5 基本使用
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
安装好VueX后,在src目录下新建一个store文件夹,再建立一个index.js文件,我们在这个js文件中来创建我们的store容器。
js
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state() {
return {
count: 0,
}
},
mutations: {
increment(state) {
state.count++
},
},
})
export default store
然后在入口文件main.js中引入store并使用它。这里的store和之前的router都可以理解为Vue的插件,都可以调用use方法。
js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')
好了,现在我们的项目中就可以通过VueX来进行全局的状态管理了,接下来我们要深入学习其中的核心概念!
2.state
2.1 保存状态
store中保存的状态同样是具有响应性的,我们把应用中的公共状态统一保存到state属性中。
js
const store = createStore({
state() {
return {
count: 0, //这里的count后面就可以被应用中所有的组件共享
}
},
})
2.2 读取状态
在项目中任意创建了一个Hello组件,现在希望在这个组件中取出刚才的count。由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
在组件中获取到store必须调用useStore这个方法(和vue-router类似,在Vue3的最新生态中推荐使用函数的方式来获取示例,而非this)
vue
//Hello组件中
<template>
<h2>{{ count }}</h2>
</template>
<script setup>
import { useStore } from 'vuex'
import { computed } from '@vue/reactivity'
let count = computed(() => {
return useStore().state.count
})
</script>
2.3 映射函数
如果你使用的是Vue2版本的配置选项写法,使用映射函数往往非常高效。当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性:
js
import { mapState } from 'vuex'
export default {
// ...
computed: {
...mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
}
2.4 组件独立的状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
3.getter
3.1设置getters
有时候我们需要从 store 中的 state 中派生出一些状态,通过计算属性即可实现,但如果有多个组件需要用到这个计算属性,我们要么复制这个计算属性,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
js
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})
3.2 读取getters
和state类似,同样可以在组件中通过store实例来读取到对应的getter。
js
//Hello中
let doneTodos = computed(()=>{
return useStore().getters.doneTodos
})
3.3 映射函数
mapGetters
辅助函数同样可以将 store 中的 getter 映射到局部计算属性中,如果你使用配置选项仍然推荐你这么做,在setup语法糖中这样使用的意义就没那么大了。
js
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
4.mutation
4.1 提交commit
VueX规定必须通过mutation来能修改state中的状态。mutations中保存了修改了state的各种方法,在组件中可以通过store.commit
来触发对应的方法,同时传入参数(这个参数叫做载荷)
js
//store
mutations: {
increment(state, n) {
state.count += n
},
},
vue
//Hello
<template>
<h2>{{ count }}</h2>
<button @click="add">点我count增加</button>
<div v-for="todo in doneTodos" :key="todo.id">
<h3>{{todo.text}}</h3>
</div>
</template>
<script setup>
import { useStore } from 'vuex'
import { computed } from '@vue/reactivity'
const store = useStore()
let count = computed(() => {
return store.state.count
})
let doneTodos = computed(()=>{
return store.getters.doneTodos
})
const add = ()=>{
store.commit('increment',5)
}
</script>
4.2 mutation必须是同步函数
VueX是规定mutations中修改状态必须是同步的,我们可以尝试写一段异步代码:
js
mutations: {
increment(state, n) {
setTimeout(() => {
state.count += n
}, 2000)
},
},
大家肯定会疑问,代码没问题呀页面也能够正确响应状态的变化,但是VueX设计思想中需要能够捕捉到mutation前后状态的快照,异步代码无法让VueX能够了解其执行时机,这样描述可能大家此时仍然不好理解,所以现在你需要记住这个结论!
5.action
action和mutation有些类似,但不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
Action 函数第一个参数接受一个与 store 实例具有相同方法和属性的 context对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。
Action第二个参数也接受一个载荷。
js
actions:{
increment(ctx,payload){
setTimeout(()=>{
ctx.commit('increment',payload)
},2000)
}
}
在组件中视图中,可以通过事件来触发action。所以之前的异步操作可以通过分发action来实现。
js
const add = () => {
store.dispatch('increment',2)
}
6.module
6.1 嵌套模块
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
js
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
6.2 命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
js
const store = createStore({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
}
}
}
})
命名空间在多人协作处理大型应用的时候还是非常具有实际意义的。