Vuex学习笔记

295 阅读11分钟

Vue笔记

五、Vuex —— 在应用很复杂时用此保管数据

5.1、理解Vuex

5.1.1、概念

专门在Vue中实现集中式状态(或数据)管理的一个 Vue 插件,对Vue应用中多个组件的共享状态进行集中式管理(读 / 写),也是一种组件间通信的方式,且适用于任意组件间的通信

5.1.2、什么时候使用?

  • 多个组件依赖于同一状态
  • 来自不同组价的行为需要变更同一状态

5.1.3、原理

在这里插入图片描述

5.1.4、安装

vue2使用vuex3版本

npm i vuex@3

vue3使用vuex4版本

npm i vuex@4

5.1.5、引入和使用

main.js

//引入
import Vuex from 'vuex'

//使用
Vue.use(Vuex)

自此创建vm时,就可以创建store配置项

vuex的代码一般放在一个专门的文件夹里,官网建议其文件夹名为store,store的代码放在文件index.js

src/store/index.js此处引入Vue,创建、引入Vuex插件并应用

//改文件用于创建Vuex中最为核心的store

//引入Vue
import Vue from "vue"
//引入Vuex
import Vuex from "vuex"
//应用Vuex
Vue.use(Vuex)

//准备actions,用于响应组件中的动作
const actions = {}

//准备mutations,用于操作数据(state)
const mutations = {}

//准备state,用于存储数据
const state = {}

//创建并暴露store
export default new Vuex.Store({
    //注意重名可以使用简写形式
    actions: actions,//配置
    mutations: mutations,//配置
    state: state,//初始化数据
    getter:getter
})

main.js,创建vm时引入store配置项

import Vue from 'vue'
import App from './App.vue'
import vueResource from 'vue-resource'
import store from './store'

Vue.config.productionTip = false
Vue.use(vueResource)

new Vue({
  el: '#app',
  render: h => h(App),
  store,//使用 store: store, 的简写形式
  beforeRender(){
    Vue.prototype.$bus = this;
  }
})

之后vc和vm就可以使用$store

5.1.6、store

store是一个容器,包含着应用中大部分的state,官方文档中说明,Vuex与单纯的全局对象有两点不同

  • Vuex的状态存储是响应式。Vue组件从store中读取状态时,如果store的状态发生改变,那么响应组件也会迅速更新
  • store中的状态无法直接改变,只能通过显式地提交(commit)mutation来改变

创建一个简单的store

const store = new Vue.Store({
	state:{
		cnt: 0
	},
	mutations:{
		increment(state){//传入 state 对象作为参数
			state.cnt++
			//通过 state 调用 cnt
		}
	}
})

获取状态对象:store.state.xxx,触发状态变更:store.commit('xxx')

在Vue组件中访问store原型

this.$store.state.xxx;
this.$store.commit('xxx');
---

组件中调用store中的状态只需要在计算属性中返回即可,触发变化只需要在组件的methods中提交mutation

5.2、Vuex核心概念和API

state、getters、mutations、actions、modules 都放在store里面

5.2.1、state

state在Vuex中用于存储数据,且存储在Vuex中的数据与存储在Vue实例中data上的数据遵循相同的规则,这种方式会及时触发相关联的 DOM 更新

const state - {
	sum:0,
	subject:'math'
}

在根组件App.vue注册store,由此将其“注入”到所有子组件中,在子组件中通过this.$store访问store原型,再通过计算属性computed返回某个状态来读取store实例中状态

const vc = {
	template: `<h1>{{number}}</h1>`,
	computed: {
		number(){
			return this.$store.state.num
		}
	}
}

这种方式比在每个组件中都注册一次各自的store再使用要简单得多

mapState辅助函数

帮助映射state中的数据为计算属性

  • 作用:协助生成计算属性,简化声明过程,使用前需要从vuex中引入
  • 使用情景:一个组件需要多个状态,并要将这些状态都声明为计算属性
  • 格式:const x = mapState({keyName_01:'value_01',keyName_02:'value_02'}),参数传入一个对象,对象中是键值对,值必须用''再用...对象展开运算符将此mapState放入计算属性computed

对象写法

import {mapState} from 'vuex'

export default {
	//省略
	computed:{
		//使用展开运算符将 mapState 在计算属性中展开
		//借助 mapState 生成计算属性,从 state 中读取数据   ---对象写法
		...mapState({he:'sum',subject:'math'}),
		//也可以 ...x
	},
	mounted(){
		const x = mapState({he:'sum',subject:'math'})
		//sum必须用'',he可以不用,解析的时候会加上
	}
}

mapState({xxx})返回一个对象

字符串数组写法

当键值名相同且其在state中存在时,还可以使用简写方式,但应当用''

state

const state = {
	sum:0,
	subject:'math'
}

mapState()

import {mapState} from 'vuex'

export default {
	//省略
	computed:{
		...mapState(['sum','subject']),    //----字符串数组写法
	}
}
//混入 computed 后展开相当于于:
/*
*   sum(){
* 		return this.$store.state.sum //0
* 	},
*   subject(){
* 		return this.$store.state.subject //'math'
* 	}  
*/

5.2.2、getters

作用:加工stata中的数据,注意,这没有改变其本身的值

const getters = {
	getRes(state){
		return state.sum+50
	}
}

mapGetters

与 mapState类似,使用前也要引入,但其映射的是getters中的数据为计算属性

5.2.3、mutations

更改Vuex的storestate状态的唯一方法是提交mutation,其中包含一个个字符串的 事件类型type 和对应的回调函数,通过回调函数对state中的数据进行更改,回调函数接收的第一个参数state

const store = new Vuex.Store({
	state:{
		number: 13
	},
	mutations:{
		reduce(state){//第一个参数是 state, 事件类型名是 reduce
			//通过 state 调用其中的数据并修改
			state.number--
		}
	}
})

使用store.commit()方法调用相应的type对state中的状态进行修改,注意type的名字是是字符串形式

store.commit('reduce')

还可以向事件类型中传入额外的形参,相应的,向store.commit()中也要传入对应类型的实参,这个参数可以是一个String或Number型变量,但更多时候最好是一个对象,这个额外的参数又叫载荷Payload

const store = new Vuex.Store({
	state:{
		number: 13
	},
	mutations:{
		reduce(state, obj){//将整个对象作为载荷
			state.number += obj.num1
		}
	}
})
//向对应的commit传参
store.commit('reduce',{//将事件类型名作为第一个实参
	num1: 100
	//因为 reduce 中传入的形参是对象,所以这里实参也该是对象
})

提交mutation的另一种方式:直接使用包含type属性的对象

store.commit({//传入一个对象型实参
	type: 'reduce',//将事件类型名放到该对象的 type 属性中
	num1: 66
})

在对象上添加新属性

  • 使用set方法:Vue.set(obj, 'newStr', 123, ...)
  • 使用解构赋值:state.obj = {...state.obj02, 'str', 123}

可以使用常量替代事件类型

mutation.js

export const getSum = 'getSum'

store.js

import Vuex from 'vuex'
import {getSum} from './mutation.js'

const store = new Vuex.Store({
	state:{
		num: 10
	},
	mutation:{
		//用一个常量作为函数名
		[getSum](state, n){
			state.num += n
		}
	}
})

mutation必须是同步函数

mapMutations辅助函数

用于在组件中提交mutation时,将组件中的 methods 映射为store.commit,这种写法需要提前在根组件注入store

import {mapMutations} from 'vuex'

export default{
	//省略
	methods:{
		...mapMutations([
			'reduce',//将 this.reduce() 映射为 this.$store.commit('reduce')

			//支持传入载荷
			'increase',//假设有个载荷为 num, 即原本为 this.increase(num)
			           //将其映射为: this.$store.commit('increase', num)
		])
	}
}

5.2.4、actions

Action 类似于 Mutation,但有区别,主要是下面两点

  • actions 用于提交 mutation, 而非像mutation那样变更状态
  • action 可以包含任意异步操作,而mutation只能包含同步操作

actions函数接受一个context对象作为参数,这个对象与store实例具有相同的属性和方法,但不是store实例本身。可以通过context对象来调用commit提交mutation等

action:{
	reduce(context){
		context.commit('reduce')
	}
}

需要多次使用某方法时,也可以使用参数解构来简化代码

actions:{
	reduce( {commit} ){
		commit('reduce')
	}
}

分发Action

触发action需要使用store.dispatch('xxx'),它返回一个Promise,actions可以用载荷或者对象方式进行分发

//载荷方式
store.dispatch('reduce',{
	num: 30
})

//对象方式
store.dispatch({
	type:'reduce',
	num: 30
})

在actions内进行异步操作

actions:{
	reduceAsync( {commit} ){
		setTimeout(()=>{
			commit('reduce')
		}, 2000)
	}
}

组件中分发Action

  • 使用this.$store.dispatch('xxx')分发
  • 使用mapActions函数将组件的 methods 映射为store.dispatch调用
import { mapActions } from 'vuex'

export default{
	...mapActions(['reduce', 'incrementBy']),//mapActions同样支持载荷
	...mapActions({
		add:'increment'//将 this.add() 映射为 this.$store.dispatch('increment')
	})
}

组合Action

当一个store.dispatch在不同模块中触发的全部action函数都完成后,它自己返回的Promise才会执行

可以通过组合多个action,来处理复杂的异步流程。这之前,需要了解到: store.dispatch()可以用来处理被触发的 action 的处理函数返回的Promise,并且它自己也返回Promise,比如

store.dispatch('actionA').then(()=>{
	commmit('reduce')
})

使用async组合多个action实现异步流程

action:{
	async actionA ( {commit} ){
		commit('fn_1', await fn_1())
	},
	async actionB ( {dispatch,commit} ){
		await dispatch('actionA');//actionA完成后再继续
		commit('fn_2', await fn_2())
	}
}

5.2.5、modules

如直观的意义一样。modules就是将store分割为数个模块,每个模块都有各自的statemutationactiongetter,同时每个模块还能继续向下分隔。

但值得注意的是,modules的声明应当在store之前。

const moduleA = {
	state: () => ({
		//...
	}),
	mutations: {
		//...
	},
	actions: {
		//...
	},
	getters: {
		//..
	}
}

const moduleB = {
	state: () => ({
		//...
	}),
	mutations: {
		//...
	},
	actions: {
		//...
	},
	getters: {
		//..
	}
}

const store = new Vue.Store({
	mudules: {
		firModule: moduleA,
		secModule: moduleB
	}
})

//使用模块:
console.log(store.state.firModule);//输出 firModuleA 的 state

模块的局部状态

1、在模块内部的mutationgetter中,接收的首个参数state是其所在模块内的局部状态对象

const moduleA = {
	state:()=>({
		number: 30
	}),
	mutations: {
		reduce(state){//此处的 state 是这个模块内的 state
			state.number++
		}
	}
}

2、模块内的action,局部状态通过context.state暴露,其根节点状态通过context.rootState暴露

const moduleA = {
	actions:{
		isSame( {state, commit, rootState} ){
			if((state.number + rootState.num)%2 === 1){
				commit('reduce')
			}
		}
	}
}

3、模块内部的getter,根节点状态rootState作为第三个参数暴露

const moduleA = {
	getters:{
		getSum (state, getter, rootState){
			return state.number + rootState.num
		}
	}
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间,这使多个模块能够对同一 mutation 或者 action 做出响应。

如果希望某个模块具有高度的封装度且更加方便被复用,那么可以添加namespaced: true,使其成为带命名空间的模块

const store = new Vuex.Store({
	modules: {
		moduleA: {
			namespaced: true,//这个模块被命名为moduleA, 需要使用 moduleA 对其调用

			state: {
				num1: 10
			},
			getter: {...},
			actions: {...},
			mutations: {...},

			//在一个模块中嵌套子模块,继承父模块命名空间
			modules: {
				//嵌套的模块不设置namespaced,则会自动继承父模块的命名空间
				//换而言之,不设置namespaced的子模块在父模块的命名空间内
				moduleB: {
					state: {
						num2: 50 //组件中获取: this.$store.state.moduleA.num2
					}
				},

				//嵌套子模块,不继承父模块命名空间
				moduleC: {
					namespaced: true,
					state:{...},
					getters: {
						fn(){...} //组件中获取: this.$store.getters['moduleA/moduleC/fn']
					}
				}
			}	
		}
	}
})
在带命名空间的模块内访问全局内容

1、想要使用全局的 state 和 getter, 则需要将rootState作为第三参数rootGetters作为第四参数传入getter,也会通过context对象的属性传入action

modules:{
	A: {//该模块命名空间为A
		namespaced: true,
		getters: {
			//该A模块的 getter 中,getters被局部化,只属于这个这个A模块
			theGetter(state, gettersm rootState, rootGetters){
				//通过模块A访问getState
				getters.getState; //对应路径: 'A/getState'

				//通过根节点访问getState
				rootGetters.getState //对应路径: 'getState'
			},
			getState:state => {...}
		}
	}
}

2、需要在全局命名空间内分发 action 或者提交 mutation 时,需要将对象{root: true}作为第三个参数传给dispatchcommit

modules: {
	A: {
		namespaced: true,
		
		actions: {
			//在带命名空间的模块中, dispatch 和 commit 均被局部化
			//可以使用 root 属性访问 根dispatch 和 根commit
			theAction ( {dispatch, commit, getters, rootGetters} ){
				dispatch('otherAction', null, {root: true}) //对应路径: 'otherAction'

				commit('otherMutation', null, {root: true}) //对应路径: 'otherMutation'
			} 
		},
		otherAction(context, payLoad){...}
	}
}
在带命名空间的模块内注册全局action

需要添加{root: true},并将这个需要注册的 action 的定义放在 handler

const moduleA = {
	namespaced: true,
	theAction: {
		root: true, //条件1:添加{root: true}
		handler(namespacedContext, payLoad){ //条件2:将要注册的theAction放入handler
			console.log(namespacedContext);//上下午信息
			console.log(payLoad)
		}
	}
}
带命名空间的绑定函数

用于简化函数mapStatemapGettersmapActionsmapMutations绑定带命名空间模块的操作

方法:将模块的空间名称字符串作为第一个参数传递给以上函数,由此,所有绑定都会自动将该模块作为上下文

computed: {
    ...mapState('some/nested/module', {
        a: state => state.a,
        b: state => state.b
    })
},
methods: {
    ...mapActions('some/nested/module', [
        'foo', // -> this.foo()
        'bar' // -> this.bar()
    ])
}

此外,还可以使用createNamespacedHelpers创建基于某个命名空间辅助函数,它会返回一个对象,其中有新的绑定在给定命名空间值上的组件绑定辅助函数

//首先引入createNamespacedHelpers
import {createNamespacedHelpers} from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}  

模块动态注册

创建store,可以使用store.registerModule方法进行注册模块,这个方法接受3个参数,第一个参数是模块名,第二个参数是模块的内容。

如果不注册嵌套模块,则第一个参数直接传字符串形式的模块名;如果注册嵌套模块,则第一个参数传入一个数组,其中依次存放父模块名、子模块名

import Vuex from 'vuex'

//创建store
const store = new Vue.Store({...})

//动态注册非嵌套模块
store.registerModule('moduleName',{
	//...
})

//注册嵌套模块
store.registerModule(['fatherModule', 'childModule'],{
	//...
})

访问状态:

//访问非嵌套模块
store.state.moduleName

//访问嵌套模块
store.state.fatherModule.childModule

模块动态卸载:

store.unregisterModule(moduleName)

检测模块是否注册成功

store.hasModule(moduleName)

保留state store.registerModule()接收的第三个参数,是一个对象,可以在其中设置perserveState:true,从而在注册信模块时,保留过去的state——即这个模块的state不会和其actionmutationgetter一起被添加到store

模块重用

需要创建一个模块的多个实例时,通过使用模块重用的方法来解决可能出现的状态对象被修改时,store或模块间数据相互污染的问题

解决方式是:使用一个函数来声明模块的状态(vue2.3.0及以上支持)

const moduleA = {
	state: ()=> ({
		//...
	}),
	//mutation、action、getter等同理
}