web应用开发中,会涉及在多个层级组件之间进行传值或者多个组件共享一个状态的问题,Vue官方提供了状态管理工具Vuex插件,主要解决状态共享和状态管理问题。
状态管理方式
vue对状态的管理是独立式的,即每个组件只负责维护自身的状态,先从一个简单的示例组件来理解状态管理,修改示例HelloWorld.vue文件如下:
<template>
<h1>计数器{{count}}</h1>
<button @click="increment">增加</button>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
页面渲染了一个按钮组件和一个文本标题,当用户单击按钮时,标题上显示的计数会自增。在VUE应用中,组件状态的管理由如下几部分组成:
1. 状态数据
状态数据是指 data函数中返回的数据,这些数据自带响应性,由其来对视图的展现进行驱动 。
2, 视图
视图是指template里面定义的视图模板,其通过声明的方式将状态映射到视图上。
3. 动作
动作是指会引起状态变化的行为,用来改变状态数据,状态数据的改动最终驱动视图的刷新。
数据、视图、动作三部分协同工作就是vue状态管理的核心。在这个状态管理模式中,数据的流向是单向的,私有的。由视图触发动作,动作改变状态、状态驱动视图,此过程如图所示:
单向数据流使用图
单向数据流这种状态管理模式简洁,对于组件不多的简单应用简洁高效,但对于多组件复杂交互场景 ,会出现以下问题:
(1)有多个组件依赖同一个状态。使用上述状态管理方法难以实现,对于嵌套的多个组件,可以通过传值的方式传递状态,对于平级的多个组件,难以实现共享同一个状态。
(2)多个组件触发动作变更一个状态。简单方式是将触发动作交给上层,对于多层嵌套的组件,需要层层向上传递事件,在顶层统一处理状态的更改,其结果是增加代码维护难度。
Vuex 就是基于这种应用场景产生,它将需要组件间共享的状态抽取出来,以一个全局的单例模式进行管理。此种模式下,可以任意获取或更改共享状态。
概述
Vuex是一个专供Vue应用程序开发的状态管理模式,采用集中式存储管理应用的所有组件的状态,并以相应的规则保证以一种可预测的方式发生变化。它是一个存放多个组件共用的数据的一个容器,存放的数据是响应式,即存放数据发生变化,各个组件都会自动更新。
每一个vuex应用的核心就是store,store基本上就是一个容器,它包含着应用中大部分的状态,它的状态存储是响应式,当组件从store中读取状态时,若 store中状态变化,则相应组件会自动更新。不能直接改变store中状态,唯一途径就是显式提交mutation。在Vuex中有默认的五个核心概念,分别是state、Getter、Mutation、Action、Module。
Vuex组成
state是数据源,所有需要的数据就保存在state对象中,可以在页面通过this.$store.state获取定义的数据与改变相应的状态变量值。
Getter类似于vue中的compute计算属性,用于监听state中值的变化,返回计算结果。可以在页面中通过this.$store.getters.xxx方式访问。
Mutation对象用于更改store中的状态,方法是显式提交mutation,即通过 this.$store.commit()提交mutation,并且Mutation中的函数必须是同步函数。
Acton对象是通过提交mutation间接更改store中的状态,此种函数可以包含任何异步的操作。
Module对象是将store分割成模块,每个模块拥有独立的state,getter,mutation,action对象,甚至是嵌套子模块,该子模块也可以同样进行分割。
Vuex安装
有三种方式可以实现Vuex的安装,分别本地引入,CDN引入,npm 安装。
(1)本地引入,通过地址https://unpkg.com/vuex下载后存放在一个文件夹中,通过script标签引入,代码如下:
//直接下载引入
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
(2) CDN引入,直接复制CDN链接引入,注意必须先引入vue,再引入vuex ,代码如下:
//CDN方式引入
<script src="https://cdn.bootcdn.net/ajax/libs/vue4.0.2/vuex.cjs.js"></script>
(3) npm安装,通过npm安装vuex,操作代码如下:
npm install vuex --save
或
npm install vue@next --save
在一个模块化的打包系统中,即使经通过npm安装了vuex ,也必须显式地通过Vue.use()来使用Vuex
简单store使用
下面的实例通过一个简单的store来存储共享状态,能够实现不同组件之间共享该状态,在某个组件内对其state进行修改,同时也会在使用该状态的其它组件中进行修改并显示。html文件主要代码如下:
<script src="./js/vue3.js"></script>
<body>
<div id="app-one">
<p @click="addNum">我是{{msg}},单击我加一</p>
<p>{{shareState.result}}</p>
</div>
<div id="app-two">
<p @click="subNum">我是{{msg}},单击我减一</p>
<p>{{shareState.result}}</p>
</div>
<script>
const { createApp,reactive}=Vue;
const store={
state: reactive({ result:0
}),
addResultAction(){
this.state.result+=1;
},
subResultAction(){
this.state.result-=1;
}
}
//加一组件
const AppOne=createApp({
//state状态
data: function(){
return {
msg: '我是AppOne',
shareState: store.state
}
},
methods: {
addNum(){
console.log(1)
store.addResultAction();
}
}
}).mount("#app-one")
//减一组件
const AppOne=createApp({
//state状态
data: function(){
return {
msg: '我是AppTwo',
shareState: store.state
}
},
methods: {
subNum(){
console.log(2)
store.subResultAction();
}
}
}).mount("#app-two")
</script>
</body>
Vuex是通过store选项提供了一种机制将状态从根组件“注入”到每一个子组件中,需调用Vue.use(Vuex)。一般是在根实例中注册store选项,该store实例会注入到根组件下的所有子组件中,且子组件能通过this.$store访问。
通过在main.js入口文件中引入store文件,并通过use()来使用store实例。在全局任何组件都可以使用store。
state
state是Vuex中最重要的一个概念,表示状态,保存在storek ,因store唯一,所以state也唯一,通常将state称为单一状态树,state中的状态是响应式的。
1.普通用法
例如在state中定义一个count状态变量初始值为0,则在其它组件中可以通过this.$store.state.count访问变量,如下代码所示:
state: {
count: 0
},
<div>count值为:{{this.$store.state.count}}</div>
也可以将状态变量通过computed计算属性来返回,这样每当store.state.count发生变化的时候都会重新计算属性,并且触发更新相关的DOM。代码如下所示:
<div>count值为:{{count}}</div>
computed: {
count(){
return this.$store.state.count
}
}
2. mapState辅助函数用法
当一个组件需要获取多个state状态的时候,如果将这些都声明为计算属性,会有冗余,可以使用mapState辅助函数来生成计算属性,简化代码的书写,代码如下所示:
<div>
<div>count值为:{{ count }}</div>
<div>countAlias值为:{{ countAlias }}</div>
</div>
<script>
import { mapState } from 'vuex'
computed: mapState({
//箭头函数
count: state=>state.count,
//传字符串参数
countAlias: 'count',
})
</script>
要使用mapState辅助函数必须通过Vuex来引入该方法,然后在mapState函数中传入一个对象,其属性可以通过箭头函数简化代码的书写,状态变量别名必须用字符串来表示。
Getters
state是用来保存数据,如果要对数据进行一系列处理然后再输出,一般我们可以写到computed中,但如果多个组件都要使某些用处理后数据,此时可以用Getters来实现,Getters可以认为是store的计算属性,同state一样是自响应,即它的依赖值发生改变,它也同时重新计算。
1. 基本用法
首先Getters可以接受一个state作为第一个参数,例如有一个学生信息列表数据studentinfo,要显示分数在90分及以上的同学的信息,可以在Getters属性中定义一个过虑后的函数filterStudent,其中传入state作为第一个参数来获得过虑后的数据,代码如下:
export default createStore({
state: {
studentInfo:[
{ name: 'zhangsan',age: 18,sex: 'male',score: 90 },
{ name: 'lis',age: 19,sex: 'female',score: 85 },
{ name: 'wangwu',age: 19,sex: 'male',score: 93 },
{ name: 'qiyi',age: 17,sex: 'female',score: 88 },
]
},
getters: {
filterStudent: state=>{
return state.studentInfo.filter(student=>student.score>=90)
}
}
})
在页面中以属性的形式访问Getters中定义的值,代码如下:
<div>过虑后的学生信息为:{{ $store.getters.filterStudent }}</div>
Getters 也可以接受其它getters作为第二个函数,例如,要获取过虑后的学生信息列表的长度,可传入 getters作为第二个参数,然后得到过虑后的数据长度。代码如下:
getters: {
filterStudent: state=>{
return state.studentInfo.filter(student=>student.score>=90)
},
filterStuLength: (state,getters)=>{
return getters.filterStudent.Length
}
},
<div>过虑后的学生有{{ $store.getters.filterStuLength }}个</div>
此时通过Getters可以重复使用,比如想在子组件child中使用filterStuLength,直接通过store.getters.filterStuLength获取。
2. mapGetters辅助函数
mapGetters辅助函数同mapState辅助函数用法类似,它仅仅是将store中的getters映射到局部计算属性中,例如,获取之前定义的filterStudent和filterStuLength, 可以表示为如下两种方式。
(1)使用对象展开运算符将getters混入computed对象中,首先可以传入数组,代码如下:
import { mapGetters } from 'vuex'
computed: {
//使用对象展开运算符将getters混入computed对象中
...mapGetters([
'filterStudent',
'filterStuLength'
])
}
然后直接调用即可获得同样的效果:
<template>
<div>
过虑后的学生信息为:{{filterStudent}}
<ul>
<li v-for="item in filterStudent" key="item.name">
<p>姓名: {{ item.name }}</p>
<p>年龄: {{ item.age }}</p>
<p>性别: {{ item.sex }}</p>
<p>分数: {{ item.score }}</p>
</li>
</ul>
<div>过虑后学生有{{ filterStuLength }}个</div>
</ul>
</div>
</template>
(2)也可以传入对象,重新取一个别名:
computed: {
//使用对象展开运算符将getters混入computed对象中
...mapGetters([
//把‘this.filterStu’映射为‘this.$store.getters.filterStudent’
filterStu: 'filterStudent',
//把‘this.filterStuLength’映射为‘this.$store.getters.filterStuLength'
filterStuLength: 'filterStuLength'
])
}
Mutations
在Vuex组件中,修改store中状态的唯一方法是通过提交Mutations。
1. 普通用法
Muttations类似于事件,每个 Mutations都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数作用就是进行状态更改,并且state作为其第一个参数,例如state中有一个count初始值为0,若要实现单击按钮使count增加1,其原理就是修改state,即在mutations 属性中定义一个使count增加1的方法add()。
mutations: {
add(state) {
//修改count状态
state.count++;
}
},
由于不能直接通过store.mutations.add()来调用,必须使用 store.commit来触发对应类型方法,所以只能通过store.commit('add')的方式来提交Mutation, 从而修改count状态变量的值。
<button @click='add'>单击加一</button>
<p>count结果为{{ $store.state.count }}</p>
methods: {
add(){
this.$store.commit('add')
}
}
通过给按钮绑定单击事件从而在事件方法内部通过this.$store.commit('add')传入要提交的add给这个Mutation。
2. 提交荷载
提交荷载即所谓向函数中传入参数,不过在vuex通过store.commit()来传入额外的参数通常被称为提交荷载,可以简单地将荷载看作一个对象。例如,单击按钮加n,这个n是通过参数传入提交方法,如下代码表示每单击一次按钮,增加10,通过this.$store.commit('add',10)提交对应的type和参数。
nytations {
add(state,n) {
//修改count 状态
state.count+=n;
}
},
methods: {
add(){
this.$store.commit('add',10)
}
}
而payload为对象的情况如下:
mutations:{
add(state,payload){
state.count+=payload.count
}
}
提交的方式有两种: 把载荷和type分开提交或者把整个对象(直接使用包含type属性的对象),都作为载荷传给commit函数
//1. 把载荷和type分开提交
store.commit('add',{ count: 10})
//2. 整个对象都作为载荷传给mutation函数
store.commit({
type: 'add',
count: 10
})
3.Mutations的使用规则
第一点为增添属性规则。在Mutations中如果要给一个对象添加一个属性要使用Vue.set()的方式,或者通过扩展运算符以对象替换老对象的方式来实现。如下state有一个student对象,它有属性name,age和sex,现在要给此对象添加一个新的address属性,可通过如下方式来添加:
export default createStore({
state: {
student: {
name: 'xiaohua',
age: 17,
sex: '女'
}
},
mutations: {
addAddress(state,address){
Vue.set(state.student,'address',address)
//或者
// state.student={...state.student,address: address}
}
}
})
然后通过this.$store.commit('addAddress','北京')来提交,最后student对象就多了一个address属性。
第二点为需要使用常量来替代Mutations事件类型的名字。如下两个代码,首先使用一个常量来定义一个Mutations事件,叫作SOME_MUTATION,并将其导出。然后在要使用的文件中引入该事件类型常量,使用es6风格的计算属性命名功能(即中括号)来使常量作业函数名。
//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
}
}
})
使用常量作为函数名(Vuex是通过store.commit('add')的方式来提交Mutations,此处提交的事件类型名为add,事件类型是以字符串方式传入),对于大型项目(事件类型名多,乱)而言,使用常量类型名易于调试排错,对于小型项目,完全可以使用字符串类型名方式。
第三点,Mutations必须是同步函数,通过提交Mutations的方式来改变状态数据,可以更明确追踪到状态的变化。如果是异步函数,无法追踪状态变化,所以规定必须是同函数。
4.mapMutations辅助函数
mapMutations辅助函数同MapState和MapGetters的使用方法类似,有了mapMutations辅助函数, 除了可以在组件中使用this.$store.commit('xxx')方式提交Mutation,还可以使用mapMutations辅助函数将组件中的methods映射为store.commit调用,例如,将mutation: add和addAddress通过辅助函数结合展开运算符使用,代码如下:
methods: {
...Mutations([
'add', //将'this.add()'映射为this.$store.commit('add')
'addAddress' //将this.addAddress(address) 映射为this.$store.commit('addAddress',address)
]),
...mapMutations({
up: 'add' //将this.up()映射为this.$store.commit('add')
})
}
Actions
Actions用于执行异步代码,Actions类似于Mutations,但是与Mutation不同之处在于: (1)Actions提交的是Mutations,而不是直接变更状态。(2)Actions可以在包含任意异步操作。
1.普通使用
(1)首先是传入的参数不同,Actions 传入的参数是context对象,它是一个与store实例有相同属性和方法的对象,可以通过context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters。例如可以使用context.commit('add')提交Mutations的add方法。代码如下:
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0,
},
mutations: {
add(state){
state.count+=1;
}
},
actions: {
add(context){
context.commit('add')
}
},
})
还可以使用ES6的参数解构简化代码,特别是当需要调用commit很多次的时候,可以简化码如下:
add({commit}){
commit('add')
}
(2)其次是调用方式不同,它使用dispatch调用,而不是使用commit调用。即Actions通过store.dispatch方法触发。
store.dispatch('add')
例如将loginfo中的message进行异步改变,则可以先在mutations属性中定义修改message的方法changeMessage,然后在actions中通过context.commit异步提交changeMessage这个Mutation。
export defatult createStore({
state: {
logInfo :{
id: 1,
messsage: '我是未修改的msg',
type: 'warn'
}
},
mutations: {
changeMessage(state){
state.logInfo.message='我是修改后的message'
}
},
actions: {
changeInfo(context){
setTimeout(()=>{
context.commit('changeMessage')
},1000)
}
}
})
然后通过使用store.dispatch('changeMessage')来分发Action,即可实现页面中message改变并且Devtool中也能检测到message的改变,从而记录正确的值。
methods: {
change(){ this.$store.dispatch('changeInfo') }
},
<button @click='change'>修改logInfo中的message</button>
<div>{{ $store.state.logInfo.message }}</div>
2.action传递参数(载荷)
简单字符串参数传递
actions: {
changeInfo(context,payload){
setTimeout(()=>{
context.commit('changeMessage')
console.log(payload)
},1000)
}
},
//分发,传递字符串参数
this.$store.dispatch('changeInfo','我是传递的参数payload')
Actions以对象方式进行分发
this.$store.dispatch({
type: 'changeInfo',
payload: '我是传递的参数payload'
})
3.组合Actions(在Actions内部使用Promise)
组合多个Actions处理复杂的异步流程,就是当数据commit之后就意味着修改成功,此时可以通知外界数据已经修改成功,并且还可以做别的操作以便于组合多个Actions,该需求的实现可以用Promise解决。因为store.dispatch可以处理被触发的Actions的处理函数返回的Promise,并且store.dispatch仍旧返回Promise,如下代码所示:
actions: {
changeInfo(context,payload){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
context.commit('changeMessage')
console.log(payload)
resolve('promise-')
},1000)
})
}
},
this.$store.dispatch('changeInfo','我是传递的参数payload').then(res=>{
console.log(res)
})
上述代码的主要作用是当数据修改成功之后,在控制台上打印promise-。它首先在Actions中返回一个Promise,当Actions运行到commit方法时,执行changeInfo函数,成功时resolve('promise-'),然后通过this.$store.dispatch().then()获取成功后的结果。
由以上可知,可以直接通过以下代码获取actionA结束后的结果,既能知晓actionA何时结束,还可在其结束时执行其它操作。
store.dispatch('actionA').then(()=>{
//....
})
在另一个actionB中也可以直接使用actionA的结果提交其它Mutations,从而达到组合多个Actions的目的。
actions: {
actionB ({ dispatch,commit}){
return dispatch('actionA').then(()=>{
commit('someOtherMutation')
})
}
}
可以通过使用async和await 简化代码的书写。
//假设getData()和getOtherData()返回的是Promise
actions: {
async attionA ({commit}){
commit('getData',await getData())
},
async actionB({dispatch}){
await dispatch('actionA') //等待actionA完成
commit('getOtherData',await getOtherData())
}
}
注意:一个store.dispatch在不同模块中可以触发多个action函数,但在此种情况下,只有所触发函数完成后,返回的Promise才会执行。
Modules
modules可以让每一个模块拥有自己的state,mutations,actions,getters,使得结构清晰,方便管理。如下所示:
const moduleA={
state: ()=>({...})
mutations: {...}
actions: {...}
getters: {...}
}
const moduleB={
state: ()=>({...})
mutations: {...}
actions: {...}
getters: {...}
}
import { createStore } from 'vuex'
const store=createStore({
modules: {
a: moduleA,
b: moduleB
}
})
//访问不同模块的state
store.state.a // moduleA的状态
store.state.b // moduleB的状态
1.基本用法
如某项目有两个模块分别是登录功能,商品购买功能,需要分别存储登录相关状态、商品相关状态,则分别定义两个文件loginStore.js和productStore.js,在两个文件中分别导出各自对应对象。在store.js文件中引入loginStore.js和productStore.js,然后在store的modules属性中使用。
//loginStore.js
const loginStore={
state: {},
getters: {},
mutations: {},
actions: {}
}
export default loginStore;
//productStore.js
const productStore={
state: {},
getters: {},
mutations: {},
actions: {}
}
export default productStore;
//store.js
import { createStore } from 'vuex'
import loginModule from './loginStore.js'
import productModule from './productStore.js'
export default createStore ({
state: {},
getters: {},
mutations: {},
actions: {},
//通过module属性引入对应的模块
modules : {
loginModule: loginModule,
productModule: productModule
}
})
执行上述代码后,在页面中可以通过store.state.模块名.属性名获取相应模块中的state。由于模块形式存在,就有了自己的命名空间。
2. 命名空间
默认情况下,模块内部的actions、mutations、getters是注册在全局命名空间,这样使得多个模块能够对同一mutations或actions做出响应。但是如果希望该模块只能够通过模块自己来访问,使其具有更高的封装度和利用性,些时需要命名空间,即通过添加namespaced: true的方式,使其成为带命名空间的模块。它的所有getters,actions及mutations都会自动根据模块注册路径调整命名。
//loginStore.js
const loginStore={
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {}
}
export default loginStore;
//productStore.js
const productStore={
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {}
}
export default productStore;
3.模块动态注册
在store创建之后,有两种注册模块方式,一种是常用的通过modules属性注册相应模块,另外一种是使用store.registerModule方法动态注册模块。
import {createStore} from 'vuex'
import loginModule from './loginStore.js'
const store=createStore({})
//注册模块loginModule
store.regisiterModule('loginModule',loginModule)
//注册嵌套模块 nested/myModule
store.regisiterModule(['nested','myModule'],{
//...
})
以上代码动态注册了当前路径下的loginModule这个模块和嵌套的模块nested/myModule。