Vuex状态管理
目标
- Vue组件间通信方式回顾
- Vuex核心概念和基本使用回顾
- 购物车案例
- 模拟实现Vuex
组件内状态管理流程
- 状态管理概念:Vue中最核心的功能分别是数据驱动和组件化,使用基于组件化的开发可以提高我们的开发效率,带来更好的可维护性,通过下面代码看下组件的基本结构
new Vue({
// state
data() {
return {
count: 0,
};
},
// view
template: "<div>{{count}}</div>",
// actions
methods: {
increment() {
this.count++;
},
},
});
- 每个组件内部都有自己的数据,模板和方法
- 数据我们又可以称之为状态,每个组件内部都可以管理自己的状态
- 模板我们又称之为视图,每个组件都有自己的视图,把状态绑定到视图上,呈现给用户
- 更改状态的部分我们可以称之为行为actions,上面描述的是单个组件内部的状态管理=>在实际开发过程中,可能多个组件都会共享状态,我们所说的状态管理其实就是通过状态集中管理和分发解决多个组件共享状态的问题
状态管理的组成
- state 驱动应用的数据源
- view 以申明方式将state映射到视图
- actions 响应在view上的用户输入导致的状态变化
- 上面的箭头代表数据的流向,此时的数据流向是单向的
- state是状态,也就是我们所说的数据,数据绑定到视图展示给用户,当用户和视图交互,通过actions更改数据后,再把更改后的数据重新绑定到视图
- 单向的数据流非常的清晰,但是多个组件共享数据的时候,会破坏这种简单的结构
组件间通信方式回顾
- 三种组件间通信方式
- 父组件给子组件传值
- 子组件给父组件传值
- 不相关组件之间传值
- 父组件给子组件传值
- 子组件中通过props接收数据
- 父组件中给子组件通过响应属性传值
- 子组件给父组件传值
- 子组件$emit调用事件
- 父组件v-on注册事件
-
不相关组件之间传值
-
创建一个Vue实例 => eventBus
- 在发布者组件中bus.$emit()
- 在订阅者组件中bus.$on
- 通过ref获取子组件(不被推荐的方式)
- $root
- $parent
- $children
- refs
- ref两个作用
- 在普通HTML标签上使用ref,获取到的是DOM
- 在组件标签上使用ref,获取到的是组件实例
简易的状态管理方案
- 问题
- 多个视图依赖同一状态
- 来自不同视图的行为需要变更同一状态
实例
- stote.js => 状态仓库(对象)
export default {
debug: true,
store: {
user: {
name: "zjl",
age: 18,
sex: "男"
}
},
setUserNameAction (name) {
if (this.debug) {
console.log("setUserNameAction triggered", name)
}
this.setUserNameAction.user.name = name
}
}
- debug属性为了方便调试,如果属性值为true,在通过action修改数据的时候会打印日志
- 集中式的状态管理,所有的状态都在store对象中进行管理,并且store是全局唯一的对象,任意的组件都可以导入store模块使用其中的状态,更改状态也是在该模块中实现的,通过特殊的手段也可以记录每次状态的变化
- componentA.vue
<template>
<div>
<h1>componentA</h1>
user name:{{ sharedState.user.name }}
<button @click="change">change Info</button>
</div>
</template>
<script>
import store from './store.js'
export default {
data () {
return {
privateState: {},
sharedState: store.state
}
},
methods: {
change () {
store.setUserNameAction("componentA")
}
}
}
</script>
- 导入store对象,将共享的状态保存到组件的sharedState中
- 当前组件也可以有自己的私有状态,保存在privateState中
- 通过store对象的setUserNameAction行为修改store中的共享状态
- componentB
<template>
<div>
<h1>componentB</h1>
user name:{{ sharedState.user.name }}
<button @click="change">change Info</button>
</div>
</template>
<script>
import store from './store.js'
export default {
data () {
return {
privateState: {},
sharedState: store.state
}
},
methods: {
change () {
store.setUserNameAction("componentB")
}
}
}
</script>
- componentA和componentB都访问了store对象中的数据,共享了状态,并且在交互的时候更改了状态的相应数据
小结:
- 约束:组件不允许直接更改srore对象的state状态属性,如果想要改变state,需要调用它action来改变store中的状态
- 上面约束的好处是可以记录store中所有发生state的变更,当记录state的变更后,就可以实现高级的调试功能,比如时光旅行和回滚功能
Vuex
Vuex概念回顾
什么是Vuex
- Vuex是专门为Vue.js设计的状态管理库
- Vuex采用集中式的方式存储需要共享的状态
- Vuex的作用是进行状态管理,解决复杂组件通信,数据共享
- Vuex集成到devtools中,提供了time-travel时光旅行历史回滚功能
什么情况下使用Vuex
- 非必要的情况不要使用Vuex
- 大型的单页应用程序
- 多个视图依赖同一状态
- 来自不同视图的行为需要变更同一状态
Vuex核心概念回顾
- 上面这张图展示了Vuex中的核心概念,并且演示了整个工作流程
先从state开始看起,state是我们管理的全局状态,把状态绑定到组件(components),也就是视图上,渲染到用户界面,展示给用户,影虎可以和视图进行交互,比如点击购买按钮进行支付的时候,这个时候通过dispatch分发actions,此处不直接提交mutations,是因为actions中可以做异步的操作,购买的时候要做异步请求,购买结束之后再通过提交mutations记录状态的更改,mutations必须是同步的,所有状态的更改必须通过mutations,这样做的目的是通过mutations可以追踪到状态的变化,阅读代码的时候更容易分析应用内部的状态改变,还可以记录每次状态的改变,实现高级调试功能,例如历史回滚的功能
小结:
- Store 仓库:它是使用Vuex应用程序的核心,每一个应用仅有一个store,它是一个容器,包含应用中大部分的状态,当然我们不能直接改变store中的状态,我们需要通过提交mutations的方式改变状态
- State 状态:保存在store中,因为store是唯一的,所以状态也是唯一的,称为专一状态树,但是所有的状态都保存在state中的话,会让程序难以维护,可以通过后续的米快来解决该问题,注意,这里的状态是响应式的
- Getter Vuex中的计算属性,方便从一个属性派生出其他的值,他内部可以通过计算的属性进行缓存,只有当依赖的状态发生改变的时候才会重新计算
- Mutation:我们知道状态的改变必须票通过提交mutation来完成
- Action:和mutation类似,不同的是action是可以进行异步的操作,内部改变状态的时候都需要提交mutation
- Module 模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象上来,当应用变得比较复杂的时候,store对象就会变的相当臃肿,为了解决以上问题,Vuex允许我们将store分割成模块,每个模块拥有自己的state,getter,mutation,action,甚至嵌套的子模块
Vuex基本结构
- 在store.js中
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // 注册插件,将Vuex的store注册到Vue的实例上
const store = new Vuex.Store({
state: {}
mutations: {}
actions: {},
modules: {
},
})
export {
store
}
- 在mian.js中
import Vue from 'vue'
import App from './App.vue'
import {
store
} from './store'
new Vue({
render: h => h(App),
components: { App },
store,
}).$mount('#app-main')
State
- 单一状态树
- 在组件中引用状态,可以使用mapState函数,该函数可以接收一个数组作为参数,也可以是对象
-
数组
-
对象: 但是可以count和msg两个属性,再使用这种方式的话就会有冲突,可以使用对象参数的形式进行映射
Getter
- Vuex中的getter就相当于组件中的计算属性
- 在组件中引用getters=>mapGetter函数的使用方法和mapState函数的使用方法类似,参数可以是数组,也可以是对象
小结:mapState和mapGetters函数返回的都是对象
Mutation
- 状态的修改必须通过mutation,并且mutation必须是同步执行的
- 在组件中触发mutation必须通过store对象的commit函数,参数一:函数名,参数二:荷载
- 在组件中需要通过commit来提交mutation,而mutation的本质是方法,所以在这里我们可以使用map方法映射到当前组件的methods中=>mapMutations,这个函数依然返回的是一个对象,这个对象中存储的是mutations映射的方法,需要注意的是,这些方法不再是计算属性,要把它放到组件的methods中,mapMutations的参数可以是数组,也可以是对象
Action
- 当需要执行异步操作的时候,需要使用到action,当异步操作执行完成后,如果需要更改状态,需要通过提交mutation来修改state,因为所有的状态修改都要通过mutation,例如我们需要异步获取商品数据的话,就需要在action中发送请求,异步执行完毕获取到商品数据之后需要再提交mutation,把数据记录到state中
- 在组件中,调用action,记住,action的调用要通过dispatch提交,就像mutation需要通过commit提交一样
- 可以通过mapActions函数一样来优化代码,使用方法和mapMutations类似,函数参数的形式也有两种,数组和对象,这里我们只演示数组的形式
Module
- 模块可以让我们把单一状态树拆分成多个模块,每个模块都可以拥有自己的state,getters,mutations,actions,甚至嵌套子模块,当状态比较多的时候,非常有用
- 在products.js中=>商品列表
const state = {
products: [
{
id: 1, title: "iphone11", price: 8000
},
{
id: 2, title: "iphone12", price: 10000
}
]
}
const getters: {
}
const mutations = {
setProducts (state, payload) {
state.products = payload
}
}
const actions = {
}
export default {
state,
getters,
mutations,
actions
}
- 在cat.js=>购物车中
const state = {}
const getters: {}
const mutations = {}
const actions = {}
export default {
state,
getters,
mutations,
actions
}
- 在store.js中注册模块
- 我们可以通过store.state.products访问到products模块中的状态(state),还把模块中的mutations记录到store内部属性_mutations中,可以通过stote.commit直接提交模块中的mutation
1.1不管是在store中直接写的getter/mutations/actions的属性或者方法,还是模块中的getter/mutations/actions,实际上都会挂载到store的_getter/_mutations/_actions属性中
1.2不获取模块中的状态store+state+模块名
,因为模块中的状态会挂载到state+模块名的对象下,和getter/mutations/actions直接挂载到store相应属性下相比,多嵌套了一层(模块)
1.3在组件中调用模块中的getter/mutations/actions和调用store中非模块的getter/mutations/actions方式一样
1.4当模块中相应mutations/actions方法和非模块的mutations/actions方法相同时时,多个重名方法都会注册到store的内部属性_mutations/_actions上,当组件中调用时,重名的函数都会一次被调用(执行)
-
如果我们想要模块具有更好的封装和复用性,我们可以给模块开启命名空间,这样将来在视图中使用模块中的成员的时候,看起来也会更清晰一些,
推荐使用带命名空间的模块
-
使用map函数导入模块
- mapState(参数一,参数二) 参数一:模块的名称,参数二:映射的属性(对象或者数组)
- mapState(参数一,参数二) 参数一:模块的名称,参数二:映射的属性(对象或者数组)
-
mapMutations(参数一,参数二) 参数一:模块的名称,参数二:映射的属性(对象或者数组)
-
小结:
带命名空间的模块,我们可以清楚的看到我们映射的state和mutations是从哪个模块中获取到的,不带命名的话就是从全局的store获取的
严格模式
之前在介绍Vuex核心概念的时候说过,所有的状态变更必须通过提交mutation,但这仅仅是一个约定,如果你想的话,你可以在组件中随时获取到$store.state.msg对其进行修改,从语法层面来说,这是没有任何问题的,但是这个破坏了Vuex的约定,如果在组件中直接修改state,那我们的devtools无法跟踪到这次状态的修改
- 开启严格模式后,如果在组件中直接修改state状态,会抛出错误
- 不要在生产环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失=>在开发环境中开启严格模式,在生产环境下关闭严格模式
插件
- 历史背景:购物车中的数据分两种情况保存
- 当用户没登录的时候,数据保存在localStorage中
- 当用户登录的时候.数据保存在数据库中
- 当购物车中每一次数据(状态)发生变化,因为状态的变化都要通过mutation来变更,所有mutations中所有的方法都要调用相同的方法,要么是改变localStorage,要么改变数据库中的数据
- 但是,需要在每个mutation中做同样的事情,有没有一种机制,在每个mutation动作完成后,都会触发一个回调函数呢?这里就引入插件的概念了
Vuex插件介绍
- Vuex的插件就是一个函数
- 这个函数接收一个store的参数,在这个函数里面我们可以注册另一个函数,让它可以在所有的mutation结束之后再执行
Vuex插件使用
-
Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数
可以根据type的名称来判断是否需要执行某个动作 -
然后像这样使用
模拟Vuex
基本结构
- 实现Vuex的store类
- 实现Vuex的install方法
install
- 所有的插件应该都有install方法
let _Vue = null
class store={}
function install (Vue) {
_Vue = Vue
}
export default {
store,
install
}
- 上面已经定义好了install方法,接下来我们要思考install方法要做什么=>在install中我们要把创建vue实例的时候传入的store对象注入到vue原型上的 s t o r e , 在 所 有 组 件 中 可 以 通 过 t h i s . store,在所有组件中可以通过this. store,在所有组件中可以通过this.store来获取到vuex中的仓库,从而可以在所有组件中共享状态
- 在install中我们获取不到vue的实例,所以这里和模拟vueRouter一样,通过混入beforeCreate来获取vue实例,从而拿到选项中的store对象
- 在beforeCreate中首先判断实例的$options中是否有store属性,如果是组件实例的话,没有store选项,只有根实例才有
store类
- 首先我们思考store类该如何实现,首先需要一个构造函数,它接收一个对象
属性
:分别的state,getters,mutations和actions,并且state是响应式的方法
:commit和diapatch分别是提交mutation和分发action
state属性
- 因为state是响应式的,我们依然使用Vue.observable()进行响应式的处理
class store={
construct(options){
// 解构赋值(带默认值)
const { state = {}, getters = {}, mutations = {}, actions = {} } = options
// 因为state是响应式的,我们依然使用Vue.observable()进行响应式的处理
this.state = _Vue.observable(state)
}
}
getters属性
- 为了外界可以访问到getters,给store实例上新增一个原型为空的getters属性
this.getters = Object.create(null)
- 将传入的getters选项的key挂载到this.getters上
mutaions属性和actions属性
- 将mutations和actions存储到对应的属性中,再commit和daipatch方法中要获取
- 这里的mutations和actions是内部属性,要在属性前面添加下划线,代表私有成员,不希望外界访问,我们之前打印store对象的时候看到过这样的属性
commit和dispatch方法
- 完整Vuex代码
let _Vue = null
class store={
construct(options){
// 解构赋值(带默认值)
const { state = {}, getters = {}, mutations = {}, actions = {} } = options
// 因为state是响应式的,我们依然使用Vue.observable()进行响应式的处理
this.state = _Vue.observable(state)
// getters对象的原型设置为null
this.getters = Object.create(null)
Object.keys(getters).forEach((key) => {
// 将相应的可以注册到this.getters对象中
Object.defineProperty(this.getters, key, {
// getters[key]代表相应的方法
get: getters[key](state)
})
})
this._mutations = mutations
this._actions = actions
}
commit(type, payload){
this._mutations[type](this.state, payload)
}
dispatchEvent(type, payload){
// 我们这里简单模拟,所以用this代表store对象的副本
this.actions[type](this, payload)
}
}
function install (Vue) {
_Vue = Vue;
_Vue.mixin({
beforeCreate () {
// this就是vue实例
if (this.$options.store) {
_Vue.prototype.store = this.$options.store
}
}
})
}
export default {
store,
install
}