状态管理
什么是状态管理
为啥说是状态,对于我们来说是数据,但是对于机器来说只是变量某个时刻的状态,所以称为状态。
复杂的状态管理
Vuex 的状态管理
管理不断变化的state本身是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
- 在这种模式下,我们的组件树构成了一个巨大的 “视图View”;
- 不管在树的哪个位置,任何组件都能获取状态或者触发行为;
- 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪;
这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想):
vuex 的设计整体保持了 数据仓库 —— 视图 —— 行为 这一单向流动过程。
这里行为分成了两部分:Mutations 和 Actions,主要是为同步行为和异步行为而分,mutations 中不能进行异步操作。
为什么设计将行为进行区分?
主要原因是当我们使用 devtools 时,devtools 可以帮助我们捕捉 mutations 的快照。但如果是异步操作,那么devtools 将不能很好的追踪这个操作什么时候会被完成。会导致devtools 记录的数据没有与页面显示的数据保持一致。所以为了处理异步操作就多添加一层 actions 专门处理异步操作。
Vuex 的安装
我们这里使用的是 vuex4.x,vuex3.x 还是默认版本的时候,安装的时候需要添加 next 获取默认版本的下一个版本。
npm install vuex@next
如果不知道当前默认版本是哪个,可以通过 npm 查看远程仓库的默认版本
npm view 包名
现在默认版本就是 4.0,可直接下载
Vuex和单纯的全局对象有什么区别呢?
- 第一:Vuex的状态存储是响应式的
- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
- 第二:你不能直接改变store中的状态
- 改变store中的状态的唯一途径就显示提交 (commit) mutation;
- 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
使用步骤:
- 创建Store对象;
- 在app中通过插件安装; ```javascript import { createStore } from “vuex”
// 创建 store 仓库对象 const store = createStore({ state() { return { rootCounter: 100 } }, mutations: { increment(state) { state.rootCounter++ } } });
export default store;
```javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 注册 store 插件,另外use 返回的是 app 对象,所以可以链式使用插件
createApp(App).use(router).use(store).mount('#app')
组件中使用store:
<template>
<div>
<h1>HelloWorld</h1>
<!-- 模板中获取 store 仓库中的数据 -->
<h2> {{ $store.state.rootCounter }}</h2>
<h2> {{ counter }} </h2>
<button @click="increase">+1</button>
</div>
</template>
<script>
export default {
methods: {
increase() {
// 通过行为修改参数数据
this.$store.commit('increment')
}
},
computed: {
// options api 获取数据仓库中的数据,计算属性也封装了过长的表达式
counter() {
return this.$store.state.rootCounter
}
}
}
</script>
单一状态树
组件获取状态
上面一句有组件中获取状态的方式,但是表达式太长了很繁琐。虽然用计算属性转换了一下,但是大量的仓库数据计算属性会和其他逻辑的计算属性杂糅在一起,一大半都是这种转换数据的计算属性,喧宾夺主了。
- 因此建议使用 vuex 的
mapState
辅助函数,自动完成了 state 到计算属性的映射。
mapState 可以将 state 以对象或者数组的方式接收,并返回一个对象,对象中属性就是映射好的同名计算属性,并且属性是函数类型。
所以我们一般使用展开运算符来将映射好的计算属性一个一个取出,注册到 computed 对象中。
<template>
<div>
<h1>HelloWorld</h1>
<!-- 通过计算属性 rootCounter 获取 state 中的数据 -->
<h2> {{ rootCounter }} </h2>
<!-- 自定义了计算属性名 -->
<h2>{{hhh}}</h2>
</div>
</template>
<script>
import { mapState } from "vuex"
export default {
computed: {
// 展开运算符取出隐射好的同名计算属性
// 返回值是{ rootCounter: function }展开取出来放到 computed:{ }中正好注册为计算属性
...mapState(['rootCounter']),
// 对象的形式,可以自定义数据的名字
...mapState({
hhh: state => state.rootCounter // 通过函数取出仓库中的数据
})
}
}
</script>
在 setup 中使用 mapState
在 setup 中和 vue-router 一样,要拿到插件提供的全局对象,需要通过 hook。拿到 store 对象需要通过 useStore hook。
setup 中单个数据转成计算属性和在 computed 选项中一样问题不大,问题就在于如果有大量仓库数据,setup 中也会写的很繁琐,需要使用 mapSate ,但是默认情况下,Vuex 并没有提供非常方便的使用mapState的方式,所以我们可以自己进行了一个函数的封装:
import { useStore } from "vuex"
import { computed } from "@vue/reactivity"
export default {
setup() {
const store = useStore()
// 单个仓库数据
const count = computed( () => store.state.rootCounter)
// 封装函数对多个数据进行批量处理
function myMapState(iterableObj) {
const store = useStore()
const obj = {} // 以一个对象做载体保存计算属性 ref 对象
for (const item of iterableObj) {
// 注意不要写成 .item,要写成 [item] 做计算
obj[item] = computed( () => store.state[item])
}
return obj
}
// 解构取出所有计算属性 ref
return { count, ...myMapState(['name', 'age', 'rootCounter'])}
}
}
为了方便后续使用,可以封装为一个 hook。
import { useStore } from 'vuex'
import { computed } from '@vue/reactivity'
const useMapState = iterableObj => {
const store = useStore()
const obj = {} // 以一个对象做载体保存计算属性 ref 对象
for (const item of iterableObj) {
// 注意不要写成 .item,要写成 [item] 做计算
obj[item] = computed(() => store.state[item])
}
return obj
}
export default useMapState
也可以通过 mapState 进行封装:
import { computed } from 'vue'
import { mapState, useStore } from 'vuex'
export function useState(mapper) {
// 拿到store独享
const store = useStore()
// 获取到对应的对象的functions: {name: function, age: function}
const storeStateFns = mapState(mapper)
// 对数据进行转换
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
// mapState 实际也是通过 this.$store.state 拿到数据的,所以我们要手动绑定 this
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
getters
某些属性我们可能需要经过变化后来使用,这个时候可以使用 getters,和计算属性类似。
getters 的属性都是函数,除了可以接收 state 对象还可以接收 getters 做参数,通过 getters 就可以调用其他的 getter 属性。
import { createStore } from "vuex"
const store = createStore({
state() {
return {
name: 'zs',
age: 18,
books: [
{ aaa: '入门放弃', price: 100 },
{ bbb: 'csapp', price: 99 }
]
}
},
getters: {
tatolPrice(state, getters) {
console.log(getters.person); // 调用其他的 getter
return state.books.filter(book => {
return book.price < 100
})
},
person(state) {
return state.name + state.age
}
}
})
export default store;
获取 getters 中的数据,也是通过 store 对象
$store.getters
-
getters 的返回函数
getters 和计算属性一样,使用的时候好像是当属性使用,但属性在 getters 中是函数形式,我们可以让 getter 返回一个函数,那么使用的时候就可以像是调用函数,并且传递参数。
getters: { totalPrice(state) { return price => { const arr = state.books.filter(book => { return book.price == price }) return arr } }, person(state) { return state.name + state.age } }
setup() { // 平常使用 getters 中的 getter console.log( useStore().getters.person); // 当 getters 中的 getter 为返回函数的形式,可以传递参数了 console.log( useStore().getters.totalPrice(100)); }
mapGetters 辅助函数
我们一般把仓库数据放到计算属性 computed 中转成 ref 对象,getters 也不例外,和 state 一样也会在 computed 中出现代码杂糅的问题。
我们需要使用mapGetters
函数来简化 ```html{{totalPrice(100)}}
{{person123}}
```html <template> <h2>{{totalPrice(100)}}</h2> <h2>{{person}}</h2> </template> <script> import { mapGetters } from "vuex" export default { setup() { const store = useStore() const person = computed( () => store.getters.person) const totalPrice = computed( () => store.getters.totalPrice) return { person, totalPrice } } } </script>
setup 中对于大量的 getter 依然需要自己封装函数。但是 getters 和 state 的代码重复度很高,所以可以考虑,让 useMapState 也适应 getters。
import { useStore } from 'vuex' import { computed } from '@vue/reactivity' const useMapper = (iterableObj, mapper = 'state') => { const store = useStore() const obj = {} // 以一个对象做载体保存计算属性 ref 对象 for (const item of iterableObj) { if (mapper == 'getters') { obj[item] = computed(() => store.getters[item]) } else { obj[item] = computed(() => store.state[item]) } } return obj } export default useMapper
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。简单使用已经在 store 中演示。
mutation 中函数不止接收 state 一个参数,第二个参数 payload 负载,表示调用时传入的参数,参数可为普通值,也可为对象。
commit 提交改变时传入参数,并且有两种编码风格。state() { return { rootCounter: 100 } }, mutations: { increment(state, payload) { state.rootCounter += payload.count } } // app.vue mothods: { // 第一种风格 add() { this.$store.commit('increment', {count: 100}), // 提交参数 // 第二种风格,直接写在一个对象里 add1() { this.$store.commit({ type: 'increment', // type表示 mutation 中的函数 count: 100 }) } }
Mutation 常量类型
有时候大家为避免 mutation 中函数名写错,会采用常量的方式进行维护函数名。
定义常量:mutation-type.js
定义mutation
提交mutation
mapMutations 辅助函数
我们也可以借助于辅助函数,帮助我们快速映射到对应的方法中:
import { useStore, 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')` }) }, setup() { const store = useStore() ...mapMutations(['increment', 'incrementBy']) } }
mutation 重要原则
一条重要的原则就是要记住 mutation 必须是同步函数
- 这是因为devtool工具会记录mutation的日记;
- 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
- 但是在mutation中执行异步操作,就无法追踪到数据的变化;
- 所以Vuex的重要原则中要求 mutation必须是同步函数;
actions
异步请求的数据我们可以在组件中获取,但是如果涉及到共享,就可以让 vuex 来管理这些异步请求的数据。
actions 是从 mutation 中特意分出来的一层,处理异步的数据,所以 actions 作为 mutation 的上游,并不会直接变更数据,而是 commit 提交到 mutation 中进行变更。
- mutation 中的变更是由 store 对象进行提交,而 actions 中是
**context**
对象。
context 是一个和 store 实例均有相同方法和属性的context对象;
- 所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;
- 但是为什么它不是 store 对象呢?这个和 Modules 有关。 ```javascript import { createStore } from ‘vuex’
const store = createStore({ state() { return { rootCounter: 100, } }, mutations: { increment(state, mutationsPayload) { state.rootCounter += mutationsPayload } }, actions: { incrementAction(context, actionsPayload) { console.log(actionsPayload); // action 也能接受参数 // 因为需要异步获取数据,所以提交更改到 mutation 是异步的 setTimeout(() => { context.commit(‘increment’, 10) }, 1000); } } })
export default store
```html <template> <h2>{{$store.state.rootCounter}}</h2> <button @click="add"> +1 </button> <button @click="add1"> +1 </button> <br> <button @click="incrementAction">+1</button> </template> <script> import { useStore } from "vuex" export default { methods: { add() { // options api中分发 actions 并且提供参数,如果是 commit 就是调用 mutation中的函数了 this.$store.dispatch('incrementAction', 123) }, add1() { // action 分发也支持对象的编码风格,参数传过去也是对象的形式 this.$store.dispatch({ type: 'incrementAction', count: 456 }) } }, setup() { const store = useStore() // composition api 分发 action const incrementAction = () => store.dispatch('incrementAction', 111) return { incrementAction } } } </script>
context 对象
context 对象中不止 commit 函数,其中还有 dispatch 等函数,dispatch 是用来分发 action 的,在 actions 中使用 dispatch 自然是用来分发其他 action,套娃分发 action。
对于 context 我们可以直接解构使用,而不用引入整个 context 对象。mutations: { increment(state, mutationsPayload) { state.rootCounter += mutationsPayload } }, actions: { incrementAction({commit, dispatch}, actionPayload) { console.log(actionPayload); dispatch('text') // 调用其他 action setTimeout(() => { commit('increment', 10) // 直接使用 commit }, 1000); }, text(context) { console.log(context); } }
mapActions 辅助函数
methods: { ...mapActions(['incrementAction', 'text']) // 数组形式 }, setup() { // return { ...mapActions(['incrementAction', 'text']) } return { ...mapActions({ // 对象形式 increment: 'incrementAction', tex: 'text' }) } }
actions 的异步操作
模板中进行 action 分发的时候,因为 action 是异步的,那在模板中怎么知道 action 异步结束了呢?
可以让 action 返回一个 promise,异步操作都在这个 promise 中完成actions: { incrementAction( {commit} ) { return new Promise((resolve) => { setTimeout(() => { commit('increment', 10) // 直接使用 commit resolve('请求成功') }, 1000); }) } }
setup() { const store = useStore() const incrementAction = () => store.dispatch('incrementAction').then( res => { console.log(res); // 请求成功 }) return { incrementAction } }
module
什么是Module?
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可
能变得相当臃肿;
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块;- 对于模块内部的 mutation 和 getter,接收的第一个参数 state 是当前模块中的局部状态对象 state
import { createStore } from 'vuex' import homeModule from './module/home' // 导入其他 store 模块 const store = createStore({ state() { return { rootCounter: 100 } }, mutations: { increment(state, mutationsPayload) { // state.rootCounter += mutationsPayload state.rootCounter ++ } }, modules: { home: homeModule // 注册模块 } }) export default store
const homeModule = { state() { return { homeCounter: 10 } }, getters: { }, mutations: { increment(state) { // 和主 store 对象中的 mutation 函数同名 state.homeCounter++ } }, actions: { } } export default homeModule
<template> <h2>{{$store.state.rootCounter}}</h2> <!-- 通过模块名获取模块中的state --> <h2>{{$store.state.home.homeCounter}}</h2> <button @click="increment">+1</button> </template> <script> import { useStore } from "vuex" export default { setup() { const store = useStore() const increment = () => store.commit('increment') return { increment } } } </script>
module 的命名空间
现在存在一个问题,主 store 对象和模块中的 mutation、getters、actions 是会混在一起的。默认情况下,模块内部的 mutation、getters、actions 注册在全局的命名空间。
- 比如 home 模块中有 gettes 为 homeincrease()。在模块中提交的方式为
$store.gettes.homeincrease
。因为混在一起,所以不用特别指定就能调用,但是这就导致除了命名压根看不出 homeincrease 这个 getter 是来自 home 模块。 - 如果和上面示例代码一样,mutation 中函数名是一样的,那么 commit 的时候,主模块和子模块 home 中的 increment 都会执行。
为了解决这种模块不清的问题,可以开启各个模块自己的命令空间,添加
namespaced: true
的方式使其成为带命名空间的模块。- 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;
```javascript
const homeModule = {
namespaced: true, // 开启命名空间
state() {
return {
} }, getters: { // 子模块中的 getter 有 4 个参数 doubleHomeCounter(state, getters, rootState, rootGetters) {homeCounter: 10
} }, mutations: { increment(state) { // 和主 store 对象中的 mutation 函数同名return state.homeCounter * 2
} }, actions: { } }state.homeCounter++
export default homeModule
```html <template> <!-- 开启命名空间模块后,已经不能直接从 getters 中获取到模块中的 getter 了 --> <h2> {{ $store.getters.doubleHomeCounter }}</h2> <!-- 需要指定模块名,用方括号计算出 getter 的位置 --> <h2> {{ $store.getters['home/doubleHomeCounter'] }}</h2> <button @click="increment">+1</button> </template> <script> import { useStore } from "vuex" export default { setup() { const store = useStore() // 指定提交 home 模块中 mutation,此时主模块中的同名 increment 不会执行 const incrementAction = () => store.commit('home/increment') // const increment = () => store.commit('increment') return { increment } } } </script>
子模块对根模块的访问
子模块中 actions 中提交 action 默认是提交到本模块中的 mutation,如果想要提交到根模块,则可以在 commit 第三个参数中设置:
{root: true}
actions: { // 6 个参数 incrementAction({commit, dispatch, state, rootState, getters, rootGetters}) { commit("increment") commit("increment", null, {root: true}) // 提交到根模块的 increment } } // 模板中分发 action 时也可以指定分发到根模块 // dispatch dispatch("incrementAction", null, {root: true})
子模块怎么获取根模块中的 state 和 getter 呢?
- getters 函数中多了两个参数,
rootState
、rootGetters
代表了根模块中的 state 和 getter - actions 中 context 对象和 state 很像,不同点就是 context 对象多了也多了两个参数
rootState
、rootGetters
,就是来访问根模块数据的。module 的辅助函数
之前对 state、getters、mutation、actions 都有对应的辅助函数 mapState、mapGetters、mapMutations、mapActions。
辅助函数默认接收的数组或对象都是针对的根模块进行匹配,怎么匹配拥有命名空间的子模块呢?
有三种写法:
- 对象参数方式下指定子模块
- 辅助函数中第一个参数可以指定子模块
通过
createNamespacedHelpers
函数对辅助函数进行修改,为子模块生成一套对应的辅助函数- 这样使用辅助函数就不用一个一个指定模块了,和最开始一样正常使用就行 ```javascript computed: { // 1.写法一: …mapState({ homeCounter: state => state.home.homeCounter }), …mapGetters({ doubleHomeCounter: “home/doubleHomeCounter” })
// 2.写法二: …mapState(“home”, [“homeCounter”]), …mapGetters(“home”, [“doubleHomeCounter”])
}, methods: { // 1.写法一: …mapMutations({ increment: “home/increment” }), …mapActions({ incrementAction: “home/incrementAction” })
// 2.写法二 …mapMutations(“home”, [“increment”]), …mapActions(“home”, [“incrementAction”]), }
```javascript import { createNamespacedHelpers } from "vuex"; // 解构获取修改后的辅助函数 const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home") computed: { ...mapState(["homeCounter"]), ...mapGetters(["doubleHomeCounter"]) }, methods: { ...mapMutations(["increment"]), ...mapActions(["incrementAction"]), }
composition api
setup() { // state 和 getter 这样是不行的,因为映射返回的是函数,在模板上就会展示出一个函数 // 所以之前我们都是放到 computed 中,转成 ref 对象,则可在模块中直接展示值。 const state = mapState(["rootCounter"]) const getters = mapGetters(["doubleHomeCounter"]) // 这两个没问题,因为本来就是函数 const mutations = mapMutations(["increment"]) const actions = mapActions(["incrementAction"]) return { ...state, ...getters, ...mutations, ...actions } }
对 useState 和 useGetters 修改
为了解决上面的问题,我们也能使用自己封装的 hook 来处理 state 和 getters。先前的 hook 没有对模块进行处理,所以需要修改一下。
import { mapState, createNamespacedHelpers } from 'vuex' import { useMapper } from './useMapper' export function useState(moduleName, mapper) { let mapperFn = mapState if (typeof moduleName === 'string' && moduleName.length > 0) { mapperFn = createNamespacedHelpers(moduleName).mapState } else { mapper = moduleName } return useMapper(mapper, mapperFn) }
import { mapGetters, createNamespacedHelpers } from 'vuex' import { useMapper } from './useMapper' export function useGetters(moduleName, mapper) { let mapperFn = mapGetters if (typeof moduleName === 'string' && moduleName.length > 0) { mapperFn = createNamespacedHelpers(moduleName).mapGetters } else { mapper = moduleName } return useMapper(mapper, mapperFn) }
const getters = useGetters("home", ["doubleHomeCounter"]) // 使用自己封装的 hook