在开发中,我们会的应用程序需要处理各种各样的数据,这些 数据需要保存在我们应用程序中的某一个位置,对于这些数据 的管理我们就称之为是 状态管理
在Vue开发中,我们使用组件化的开发方式
在组件中我们定义data或者在setup中返回使用的数据, 这些数据我们称之为state
在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View
在模块中我们会产生一些行为事件,处理这些行为事件时, 有可能会修改state,这些行为事件我们称之为actions
JavaScript开发的应用程序,已经变得越来越复杂了,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏
- 多个视图依赖于同一状态
- 来自不同视图的行为需要变更同一状态
此时管理不断变化的state本身是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪
此时, 我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例对象的方式来管理,即将这些数据转变为全局对象来进行使用
- 在这种模式下,我们的组件树构成了
一个巨大的 “试图View” - 不管在树的哪个位置,任何组件都能获取状态或者触发行为
- 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会 变得更加结构化和易于维护、跟踪
从上图可以得知在vue中,vuex有5大核心:
- state ---- 存储全局状态
- getters --- store中的计算属性
- mutations --- 只能是同步操作
- actions ---- 执行异步操作
- modules
mutations中只能使用同步操作,是因为vue devtool会记录mutation中的变化,形成对应的快照,以便于我们进行调试
但是如果mutation中存在异步操作数据,会导致devtool中的数据更新不一致,不利于调试
使用
# 安装 --- 如果需要使用的是vuex4.x,安装的时候需要添加 next 指定版本
npm i vuex@next
一般我们会将我们书写的操作vuex的代码存放在store文件夹下
store
创建Store
每一个Vuex应用的核心就是store(仓库):
- store本质上是一个全局对象,它包含着你的应用中大部分的状态(state)
Vuex和单纯的全局对象的区别:
Vuex的状态存储是响应式的- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新
- 不推荐直接改变store中的状态
- 改变store中的状态的唯一途径就显示提交 (commit) mutation
- 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具(如devTool)帮助我们更好的管理应用的状态
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './routes'
import store from './store'
// vuex本质上也是vue的一个插件 --- 挂载到vue上以后会在所有的实例上生产一个$store对象来帮助我们访问vuex
createApp(App).use(router).use(store).mount('#app')
v1
store.js
import { createStore } from 'vuex'
const store = createStore({
// state是一个返回对象的函数
// 所有的vuex数据存放在state函数返回的对象中
state() {
return {
counter: 0
}
}
})
export default store
App.vue
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
increment() {
// 虽然这么做并不会报错,但是这样做vuex并不推荐
this.$store.state.counter++
},
decrement() {
this.$store.state.counter--
},
}
}
</script>
v2
store.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
counter: 0
}
},
// 通过mutations函数来修改state
mutations: {
// mutations中的函数会被vuex在合适的时间进行回调
// 会将当前vuex实例的state对象作为参数进行传入
increment(state) {
state.counter++
},
decrement(state) {
state.counter--
}
}
})
export default store
App.vue
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
increment() {
// 使用commit函数,触发mutations中的对应函数
this.$store.commit('increment')
},
decrement() {
this.$store.commit('decrement')
},
}
}
</script>
单一状态树
Vuex 使用单一状态树:
- 用一个对象就包含了全部的应用层级别的状态
- 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源
- 这也意味着,每个应用将仅仅包含一个 store 实例
- 我们可以使用module来对store进行进一步的拆分
单一状态树的优势:
- 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难
- 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便 的管理和维护
mapState
如果我们在模板中每次都需要通过$store.state.xxx的方式来访问store属性的话,会比较繁琐,所以vuex提供了mapState的辅助函数
<template>
<div>
<h2>{{ name }}</h2>
<h2>{{ age }}</h2>
<h2>{{ counter }}</h2>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'App',
computed: {
// 1. 使用方式1,使用数组作为参数进行传递
...mapState(['name', 'age', 'counter'])
}
}
</script>
<template>
<div>
<h2>{{ sName }}</h2>
<h2>{{ sAge }}</h2>
<h2>{{ sCounter }}</h2>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'App',
computed: {
// 使用方式2: 传递对象
// 1. 可以自定义需要使用的store成员的名称
// 2. 可以对获取到的数据进行二次加工
...mapState({
sName: state => state.name,
sAge: state => state.age,
sCounter: state => state.counter * 2,
})
}
}
</script>
mapState一般需要和computed一起结合使用
// mapState返回的是对象,key是属性名,value是一个get函数,这个get函数会通过this.$store去取对应的属性值
...mapState(['name', 'age', 'counter'])
// 实际编译后的结果是类似如下的对象(伪代码)
{
name() {
// mapState内部依旧是使用this.$store来取数据的
return this,$store.name
},
age() {
return this,$store.age
},
counter() {
return this,$store.counter
}
}
setup中使用mapState
hooks/useMapState.js
import { mapState, useStore } from 'vuex'
import { computed } from 'vue'
export default function(mapper) {
// 在setup函数中可以通过useStore这个hook函数来获取store对象
const store = useStore()
// mapState的参数无论是对象还是数组,他们的返回值结构都是一致的
// { key: get函数, key: get函数 }
// 所以我们导出的方法的参数即支持对象,也支持函数
const mapStateFns = mapState(mapper)
const storeState = {}
Object.keys(mapStateFns).forEach(key => {
// 之所以需要使用computed函数进行包裹的目的是为了导出对象的value
// 使用通过computed函数导出的ref对象,这样才可以在store中数据发生改变的时候
// 自动进行监听,并自动更新所有的依赖
storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
})
return storeState
}
hook使用者
<template>
<div>
<h2>{{ name }}</h2>
<h2>{{ age }}</h2>
</div>
</template>
<script>
import useMapState from './hooks/useMapState'
export default {
name: 'App',
setup() {
return {
// 自定义的hook函数的参数即支持数组,也支持对象
...useMapState(['name']),
...useMapState({
age: store => store.age
})
}
}
}
</script>
getters
某些属性我们可能需要经过变化后来使用,这个时候可以使用getters
getters类似于store中的computed
基本使用
store.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
books: [
{
name: 'book1',
price: 32,
count: 3
},
{
name: 'book2',
price: 45,
count: 5
},
{
name: 'book3',
price: 54,
count: 2
}
]
}
},
getters: {
// getters中定义的是函数
/*
1. 参数1: state对象
2. 参数2: getters对象,用于在getters中进行计算的时候可以使用其它的'计算值'
*/
totalPrice(state, getters) {
return (state.books.reduce((total, book) => total + book.price * book.count, 0) * getters.discount).toFixed(2)
},
discount() {
return 0.95
}
}
})
export default store
使用者
<template>
<div>
<!--
和计算属性一样,虽然totalPrice是一个函数,
但是使用的时候,像一个属性一样去使用即可
-->
<h2>{{ $store.getters.totalPrice }}</h2>
</div>
</template>
很多时候我们可能需要在进行计算的时候,需要添加限制条件,
此时我们可以让getters返回一个函数,通过返回的函数来接收我们需要的参数
store.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
books: [
{
name: 'book1',
price: 32,
count: 3
},
{
name: 'book2',
price: 45,
count: 5
},
{
name: 'book3',
price: 54,
count: 2
}
]
}
},
getters: {
totalPrice(state) {
// 返回函数,让getter函数可以接收外界传入的参数
return v => {
return (state.books.reduce((total, book) => book.count < v ? total + book.price * book.count : 0, 0)).toFixed(2)
}
}
}
})
export default store
使用者
<template>
<div>
<!-- 进行参数传递 -->
<h2>{{ $store.getters.totalPrice(5) }}</h2>
</div>
</template>
mapGetters
和mapStore一样,vuex提供了mapGetters函数来便于我们进行使用
数组写法
import { mapGetters } from 'vuex'
export default {
name: 'App',
computed: {
...mapGetters(['totalPrice'])
}
}
对象语法
import { mapGetters } from 'vuex'
export default {
name: 'App',
computed: {
...mapGetters({
// 这里的vlaue直接给key的name即可
// 不需要传递一个函数,这和mapStore函数的对象写法是不一致的
totalPrice: 'totalPrice'
})
}
}
和之前mapStore封装的hook函数一样的思路
import { mapGetters, useStore } from 'vuex'
import { computed } from 'vue'
export default function(mapper) {
const store = useStore()
const mapStateFns = mapGetters(mapper)
const storeState = {}
Object.keys(mapStateFns).forEach(key => {
storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
})
return storeState
}
将useGetters和useStore进行整合
useMapper.js
import { mapGetters, mapState, useStore } from 'vuex'
import { computed } from 'vue'
export default function(mapper, mapFn) {
const store = useStore()
const mapStateFns = mapFn === 'state' ? mapState(mapper) : mapGetters(mapper)
const storeState = {}
Object.keys(mapStateFns).forEach(key => {
storeState[key] = computed(mapStateFns[key].bind({ $store: store }))
})
return storeState
}
useState.js
import useMapper from './useMapper'
export default function(mapper) {
return useMapper(mapper, 'state')
}
useGetters.js
import useMapper from './useMapper'
export default function(mapper) {
return useMapper(mapper, 'getter')
}
index.js
import useGetters from './useMapper'
import useState from './useState'
export {
useGetters,
useState
}
使用者
<template>
<div>
<h2>{{ totalPrice(5) }}</h2>
</div>
</template>
<script>
import { useGetters } from './hooks'
export default {
name: 'App',
setup() {
return {
...useGetters(['totalPrice'])
}
}
}
</script>
mutations
参数传递
store.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
counter: 0
}
},
mutations: {
incrementN(store, payload) {
store.counter += payload.step
},
decrementN(store, payload) {
store.counter -= payload.step
}
}
})
export default store
使用者
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment">+10</button>
<button @click="decrement">-10</button>
</div>
</template>
<script>
import { useStore } from 'vuex'
export default {
name: 'App',
setup() {
const store = useStore()
const increment = () => store.commit('incrementN', { step: 10 })
// 这是另一个提交方式
const decrement = () => store.commit({
// type属性中书写的是事件名
type: 'decrementN',
step: 10
})
return {
increment,
decrement
}
}
}
</script>
mapMutations
和mapGetters和mapStore一样,vuex为mutation提供了辅助函数mapMutations
options api中的使用
数组写法
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="incrementN({step: 10})">+10</button>
<button @click="decrementN({step: 10})">-10</button>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'App',
methods: {
// 注意: mapMutations解构出来的函数是不需要交给computed的
// 因为mapMutations返回的是类似于 {key: 事件处理函数, key: 事件处理函数} 格式的对象
// 所以mapMutations返回的函数,可以直接合并到methods中直接使用
...mapMutations(['incrementN', 'decrementN'])
}
}
</script>
对象写法
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment({step: 10})">+10</button>
<button @click="decrement({step: 10})">-10</button>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'App',
methods: {
...mapMutations({
increment: 'incrementN',
decrement: 'decrementN'
})
}
}
</script>
composition api中的使用
数组语法
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="incrementN({step: 10})">+10</button>
<button @click="decrementN({step: 10})">-10</button>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'App',
setup() {
const mutations = mapMutations(['incrementN', 'decrementN'])
return {
...mutations
}
}
}
</script>
对象语法
<template>
<div>
<h2>{{ $store.state.counter }}</h2>
<button @click="increment({step: 10})">+10</button>
<button @click="decrement({step: 10})">-10</button>
</div>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
name: 'App',
setup() {
const mutations = mapMutations({
increment: 'incrementN',
decrement: 'decrementN'
})
return {
...mutations
}
}
}
</script>