Vuex状态管理
组件状态管理及组件间通信回顾
状态管理
状态集中管理和分发,解决多个组件共享状态的问题
状态自管理应用包含以下几个部分
-
state
:驱动应用的数据源 -
view
:以声明方式将state
映射到视图 -
actions
:响应在view
上的用户输入导致的状态变化。
单向数据流示意图
组件间通信
Vue常见的组件间通信方式有三种
- 父组件给子组件传值
- 子组件给父组件传值
- 不相关组件之间传值
其他一些通信方式(通常不推荐)
- $root
- $parent
- $children
- $refs
- 在普通HTML标签上使用ref,获取到的是DOM
- 在组件标签上使用ref,获取到的是组件实例
父组件给子组件传值
- 子组件通过
props
接收数据 - 父组件通过给子组件设置相应的属性传值
子组件向父组件传值
- 子组件通过
this.$emit(event, param)
方式触发自定义事件,并携带参数,以此向父组件传值 - 父组件通过给子组件注册相应的自定义事件,并接收事件传递的参数来获取子组件传递的值
- 父组件在注册子组件自定义事件时,也可以通过
$event
获取事件传递的参数
- 父组件在注册子组件自定义事件时,也可以通过
不相关组件之间传值
不相关组件之间的传值以
eventbus
的方式进行
- 通过
eventbus.$on(eventname, (param) => {})
注册自定义事件,并在事件被触发时通过注册的回调函数获取参数传递 - 通过
eventbus.$emit(eventname, param)
触发自定义事件,传递参数
Vuex
什么是Vuex?
- Vuex是专门为Vue.js设计的状态管理库
- Vuex采用集中式的方式存储需要共享的状态
- Vuex的作用是进行状态管理,解决复杂组件通信、数据共享
- Vuex集成到了devtools中,提供time-travel时光旅行历史回滚功能
什么情况下需要使用Vuex?
非必要的情况下不要使用Vuex,Vuex增加了一些概念,当项目相对简单的时候,使用Vuex会使业务处理变的复杂
- 大型的单页应用程序可以使用Vuex
- 多个视图依赖同一状态
- 不同视图的行为需要变更同一状态
Vuex核心概念
Store
Vuex的核心,每个应用只有一个Store,是存储状态的容器
Vuex通过在Vue根实例中注册store选项,提供了一种机制将状态从根组件注入到每个子组件中(需要使用Vue.use(Vuex)
),在子组件中通过this.$store
进行访问
State
单一状态树,存储所有的状态数据,并且是响应式的
在组件中可以通过this.$store.state
来访问State中的状态,一个简单的访问状态的方式是使用计算属性返回状态的值
当一个组件中需要访问的状态数量较多时,频繁的导入this.$store
或者声明计算属性就会显得重复和冗余,Vuex提供了辅助函数mapState
来帮助生成计算属性
mapState
可以接收对象或者数组作为参数,指定需要访问的状态名称,并返回包含状态对应计算属性的对象
-
使用示例
// 引入 mapState 辅助函数 import { mapState } from 'vuex' export default { // ... // 1. 传入对象方式 computed: mapState({ // 箭头函数可使代码更简练 count: state => state.count, // 传字符串参数 'count' 等同于 `state => state.count` // 映射状态别名 countAlias 避免与局部状态名冲突 countAlias: 'count', // 为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } }) // 2. 传入数组方式 computed: mapState([ // 映射 this.count 为 store.state.count 'count' ]) // 3. 与局部计算属性混用时 使用展开运算符 computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) } }
tips:使用Vuex并不意味着要将组件所有的状态都放入Store中,组件可以保留自己的局部状态,如果这个状态只属于这个组件
Getter
Getter用于对State做一些处理,并返回处理后的值,有点类似于计算属性
Getter通过Store
的getters
选项进行定义,接收state
作为默认参数,并返回处理后的结果
// 官方示例
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
-
Getter也可以接收
getters
作为第二个参数,来获取其他的定义的Gettergetters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } }
-
Getter可以返回一个接收参数的处理函数,在访问时传入参数,并返回相应的处理结果
getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } }
在组件中可以通过this.$store.getters
来访问定义的Getter,类似State,一个简单的访问Getter的方式是使用计算属性返回Getter的值
Vuex同样提供了辅助函数mapGetters
来帮助生成计算属性,使用方式与mapState
类似
-
使用示例
import { mapGetters } from 'vuex' export default { // ... computed: { // 使用对象展开运算符将 getter 混入 computed 对象中 // 接收数组方式 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) // 接收对象方式 ...mapGetters({ // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount` doneCount: 'doneTodosCount' }) } }
组件中访问State通常也会使用计算属性来返回State的值,那么为什么要使用Getter而不直接使用计算属性呢?
当多个组件中访问某个State都要进行相同的处理时,将这些处理逻辑集中抽取到Getter中更合适一些
Mutation
Mutation用来对State进行修改,每一个Mutation都包含一个字符串的事件类型(type)和回到函数(handler)
所有状态的修改必须通过提交Mutation来完成
Mutation必须是同步的,这样才可以追踪状态的变化,不要在Mutation中执行异步操作
Mutation通过store
的mutations
选项进行定义,接收state
与payload
作为参数,并在其中对state
进行修改
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
})
一些小规范
-
使用常量来定义Mutation的type,并将这些定义的常量放在单独文件统一管理,这样可以使Mutation的定义一目了然
import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名 [SOME_MUTATION] (state) { // mutate state } } })
- payload是调用Mutation时传入的额外参数,可以是任意类型的值,但通常建议传入一个对象,这样可以包含多个字段,且携带了属性名增加了可读性
在组件中可以通过this.$store.commit(mutationType, payload)
的方式来调用Mutation,也可以通过传入对象的方式来调用
this.$store.commit({
type: mutationType,
...payload
})
此时传入的整个对象会被当成payload
传个相应的Mutation
Vuex提供了mapMutations
辅助函数,来帮助我们将mutations
的commit
调用映射到组件的methods
中
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Action
Action用来执行异步操作更改状态,通过Action来执行异步操作逻辑,然后调用Mutation来更改状态(所有状态都必须通过Mutation来修改)
Action通过store
的选项actions
进行定义,接收context
和payload
作为参数,执行异步操作,并调用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
在组件中,通过使用this.$store.dispatch(action, payload)
来分发一个Action
Vuex提供了mapActions
辅助函数,帮助我们将对Action的dispatch
调用映射到组件methods
中
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
Module
Vuex使用单一状态树,当项目变的复杂,Store中存储的状态数量变的庞大而臃肿,这时可以将Store分割成多个Module,每个Module拥有自己的State、Mutation、Action
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
moduleA,
moduleB
}
})
store.state.moduleA.xxx // -> moduleA 的状态
store.state.moduleB.xxx // -> moduleB 的状态
对于Module的Mutation
- 第一个参数
state
是Module的局部state
对于Module的Getter
- 第一个参数
state
是Module的局部state
- 第二个参数
getters
是Module的局部getters
- 第三个参数
rootState
是根节点状态
对于Module的Action
context.rootState
是根节点状态
默认情况下,模块的action、mutation、getter注册在全局命名空间,可以通过为模块添加
namespace: true
选项为模块开启独立命名空间
对于开启了命名空间的模块,在组件中访问其state、action、mutation、getter时,需要额外添加命名空间,对于使用辅助函数来映射访问时,可以将命名空间字符串作为第一个参数传给辅助函数,例如
computed: {
...mapState('moduleA', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('moduleA', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
也可以使用createNamespacedHelpers
创建基于某个命名空间的辅助函数
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('moduleA')
export default {
computed: {
// 在 `moduleA` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `moduleB` 中查找
...mapActions([
'foo',
'bar'
])
}
}
模块的动态注册
在Store创建之后,可以通过store.registerModule
方法动态注册模块
import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
严格模式
我们约定,所有对State的修改都应当通过Mutation来进行
然而在代码层面,并没有强制限制对State的直接修改
我们可以通过
store
的strict: true
选项来开启严格模式,在严格模式下,所有非Mutation引起的状态变更,都将在控制台抛出错误(但状态仍旧会被修改)
不要在生产环境下启用严格模式,严格模式会深度监测状态树来检测不合规范的状态变更,造成性能损耗
strict: process.env.NODE_ENV !== 'production'
Vuex使用案例
这里通过实现一个购物车案例来演示Vuex的使用
购物车案例模板项目 https://github.com/goddlts/vuex-cart-demo-template.git
使用Vuex实现后的项目 https://gitee.com/rpyoyo/vuex-cart-demo-template.git
基本功能
- 商品列表
- 展示商品的列表
- 可以通过加入购物车按钮将相应的商品加入购物车中,购物车中没有该商品则增加商品,如果已有则增加数量
- 我的购物车
- 展示添加到购物车中的商品列表
- 通过删除按钮可以移除指定商品
- 汇总商品总数及价格
- 购物车列表
- 展示购物车中的商品列表
- 通过删除按钮可以移除指定商品
- 可以改变商品的数量
- 可以单独勾选、全选商品(全选的状态根据商品勾选情况变化)
- 汇总当前已勾选的商品的数量及价格(与我的购物车中的不同)
具体实现
通过分析基本功能,将整个状态分割成商品
products
与购物车cart
两个模块
在store
对应目录下创建modules
目录,并分别新建products.js
与cart.js
两个模块文件,分别定义相应的state
、getters
、mutations
、actions
,开启命名空间,并使用export default
导出
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
在store
的modules
选项中注册模块
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart'
import products from './modules/products'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
cart,
products
},
})
products模块的定义与实现
// store/modules/products.js
import axios from 'axios'
const state = {
products: []
}
const getters = {}
const mutations = {
setProducts (state, payload) {
state.products = payload
}
}
const actions = {
async getProducts ({ commit }) {
const { data } = await axios.get('http://127.0.0.1:3000/products')
commit('setProducts', data)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
cart模块的定义与实现
// store/modules/cart.js
const state = {
cartProducts: []
}
const getters = {
totalCount ({ cartProducts }) {
return cartProducts.reduce((a, b) => a + b.count, 0)
},
totalPrice ({ cartProducts }) {
return cartProducts.reduce((a, b) => a + b.count * b.price, 0)
},
checkedCount ({ cartProducts }) {
return cartProducts.reduce((a, b) => a + (b.isChecked ? b.count : 0), 0)
},
checkedPrice ({ cartProducts }) {
return cartProducts.reduce((a, b) => a + (b.isChecked ? b.count * b.price : 0), 0)
}
}
const mutations = {
addToCart (state, payload) {
// 1. 没有该商品,添加至购物车,设置默认数量,是否勾选等属性
// 2. 如果已有该商品,增加数量
const product = state.cartProducts.find(item => item.id === payload.id)
if (product) {
product.count++
product.totalCount += product.price
} else {
state.cartProducts.push({
...payload,
count: 1,
isChecked: true,
totalPrice: payload.price
})
}
},
deleteFromCart (state, payload) {
const index = state.cartProducts.findIndex(item => item.id === payload.id)
index !== -1 && state.cartProducts.splice(index, 1)
},
updateAllProductsChecked (state, payload) {
state.cartProducts.forEach(element => {
element.isChecked = payload.checked
})
},
updateProductChecked (state, payload) {
const product = state.cartProducts.find(item => item.id === payload.id)
product && (product.isChecked = payload.checked)
},
updateProduct (state, payload) {
const product = state.cartProducts.find(item => item.id === payload.id)
if (product) {
product.count = payload.count
product.totalPrice = product.count * product.price
}
}
}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
在相应的Vue组件中,通过Vuex辅助函数,将定义的state
、getters
、mutations
映射到computed
与methods
中
以cart.vue
组件为例
// cart.vue
...
本地存储(持久化)
通过Vuex中定义了状态并在Vue组件中使用,基本的状态管理功能就完成了,但当我们刷新页面时,之前对状态的操作就会丢失,这个时候就需要对状态进行持久化,可以将状态通过localStorage进行本地存储
如果项目比较简单,我们可以针对单独的模块对其状态进行存储与使用
如果项目比较复杂,我们通常会对整个单一状态树进行存储,例如使用
vuex-persist
之类的第三方库
持久化实现原理
我们需要在Vuex状态初始化的时候,将我们存储的状态恢复到store中,同时我们需要在每次状态发生变化时,将新的状态更新到存储中,我们知道Vuex的状态必须通过
mutation
来进行修改,我们可以使用store.subscribe((mutation, state) => {})
,它会在每次mutation
完成后执行,并获取到更新后的状态
// 定义一个 persist 持久化插件
const persist = (store) => {
store.subscribe((mutation, state) => {
// 将更新后的 state 存入 localStorage
localStorage.setItem('vuex-persist', JSON.stringify(state))
})
}
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
cart,
products
},
plugins: [persist] // plugins 选项中注册插件
})
// 从 localStorage 中读取保存的 state
const state = JSON.parse(localStorage.getItem('vuex-persist'))
// store 实例创建后替换 state
state && store.replaceState(state)
export default store
Vuex简单模拟实现
基本结构
通过对Vuex基本使用的分析,我们可以知道Vuex的基本结构
-
Vue.use(Vuex)
- Vuex是一个对象,需要通过
Vue.use()
进行注册,因此需要实现install
方法
- Vuex是一个对象,需要通过
-
const store = new Vuex.Store({ state, getters, mutations, actions })
- Vuex对象包含
Store
类,用于创建store
实例,接收state
、getters
、mutations
、actions
等选项
- Vuex对象包含
-
$store
- 组件中通过
$store
对state
等的访问,commit()
提交mutation
,dispatch
分发action
等
- 组件中通过
// myvuex/index.js
// Vuex基本结构
let _Vue = null
class Store {
}
const install = (Vue) {
_Vue = Vue
}
export default {
Store,
install
}
install
const install = (Vue) => {
_Vue = Vue
_Vue.mixin({
beforeCreate () {
if (this.$options.store) {
// 挂载 Vue 选项上的 store 到 Vue 原型上
_Vue.prototype.$store = this.$options.store
}
}
})
}
Store类
class Store {
constructor (options) {
const {
state = {},
getters = {},
mutations = {},
actions = {}
} = options
// state 是响应式的
this.state = _Vue.observable(state)
this.getters = Object.create(null)
// 转换 getters[key] 为 $store.getters.key 的访问方式
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](state)
})
})
this._mutations = mutations
this._actions = actions
}
commit (type, payload) {
this._mutations[type](this.state, payload)
}
dispatch (type, payload) {
this._actions[type](this, payload)
}
}