什么是状态管理
在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是 状态管理。
在vue项目中我们是如何管理自己的状态呢?
- 在Vue开发中,我们使用组件化的开发方式。而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state。
- 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View。
- 在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions。
复杂的状态管理
JavaScript开发的应用程序,已经变得越来越复杂了。JavaScript需要管理的状态越来越多,越来越复杂。这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等。也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页。当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏。多个视图依赖于同一状态。来自不同视图的行为需要变更同一状态。
我们是否可以通过组件数据的传递来完成呢?
对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态。
但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的。
比如兄弟组件如何共享数据呢?
当然可以通过事件总线的方式传递数据。但是状态多了,也不好管理。所以我们就需要vue官方提供的状态管理库vuex。
Vuex的状态管理
下面这张图就可以概括vuex的一切了。
如果您不打算开发大型单页应用,您最好不要使用 Vuex。就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。
vuex使用
下面我们就来介绍vuex的使用了。
这里我们介绍的是vuex4.x的版本,所以需要安装 npm install vuex@next --save
。
每一个Vuex应用的核心就是store(仓库):store本质上是一个容器,它包含着你的应用中大部分的状态(state)。
Vuex和单纯的全局对象有什么区别呢?
- Vuex的状态存储是响应式的
- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新。
- 你不能直接改变store中的状态
- 改变store中的状态的唯一途径就显示提交 (commit) mutation。
- 而且异步数据,需要在action中提交mutation, 在项目中通过dispatch将数据提交到action。 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态。
vux的基本使用
- 通过
createStore
API创建一个store对象,用于保存数据。
- 在app.use中注册这个store对象。 下面我们来介绍vuex中的5个核心
state
用于定义项目的共享数据。参数为对象或者函数。我们建议使用函数作为state的值。
const store = createStore({ state() { return { rootCounter: 100 } } })
我们项目中获取到定义的state呢?
- 在template中,我们自己通过
$store.state
即可获取state的值。
- 在optionsAPI中,我们可以通过
this.$store.state
获取state的值。
- 在compositionAPI中,我们可以通过vuex提供的
useStore
API获取到state的值。 一般我们都会都会将值放在computed中。
computed: { name() { return this.$store.name } }
如果我们有很多数据需要在store中获取,直接通过上述方法取值,比较繁琐,所以我们可以通过vuex提供的对应的map方法,mapState
。
mapState做了什么呢?
他就是将store对象中对应的state做一层映射。他可以传入一个数组或者一个对象。并且返回一个对象,里面是计算函数。
- 传入数组时,我们将state中的状态作为数组中的元素(字符串)
// 直接使用 <h2>Home:{{ age }}</h2> <h2>Home:{{ name }}</h2> const store = createStore({ state() { return { name: 'zh', age: 20 } } }) computed: { ...mapState(["name", "age"]) }
- 传入对象时,我们可以指定对应state状态的名字。防止和组件本身的data数据名同名。
<h2>Home:{{ sAge }}</h2> <h2>Home:{{ sName }}</h2> const store = createStore({ state() { return { name: 'zh', age: 20 } } }) computed: { ...mapState({ sAge: (state) => state.age, sName: (state) => state.name, }) }
我们发现,在optionsAPI的computed中使用mapState非常简单,但是我们知道在compositionAPI的computed需要传入一个函数返回的是一个计算属性的值。这样mapState在其中就不好使用了。
下面我们来看看情况
<h2>{{storeState.age}}</h2> <h2>{{storeState.name}}</h2> <h2>{{storeState.counter}}</h2> const storeState = computed(() => ({ ...mapState(['counter', 'name', 'age']), }))
从上面可以看出storeState
中的属性都是一个个函数。
这是为什么呢?
因为mapState返回的是一个对象,他的属性就是返回的一个个计算函数。
可能有人会说,那么我们就在template使用的时候当成函数调用就行了啊。哈哈,我们来试试。
<h2>{{storeState.age()}}</h2> <h2>{{storeState.name()}}</h2> <h2>{{storeState.counter()}}</h2>
看到这,还有人不死心,说可以在调用的时候绑定this啊,将store对象绑定到该函数中啊。那确实。再来试试。
<h2>{{storeState.age.call({$store: store})}}</h2> <h2>{{storeState.name.call({$store: store})}}</h2> <h2>{{storeState.counter.call({$store: store})}}</h2> setup() { const store = useStore() const storeState = computed(() => ({ ...mapState(['counter', 'name', 'age']), })) return { storeState, store, } }
这时候就可以展示出具体内容了。不容易啊。可是这样的实现,还不如直接通过store.state直接一个个取出数据呢。所以我们需要封装一个hook。步骤其实就是上面的实现过程。
import { useStore, mapState } from 'vuex' import { computed } from 'vue' const useState = function(mapper) { // mapper: Array | Object const store = useStore() //将返回一个对象 const storeStateFns = mapState(mapper) // 用于存放获取到的state.属性: ref对象 键值对 const storeState = {} Object.keys(storeStateFns).forEach(item => { // 这我们知道辅助函数的内部是通过this.$store来实现的 // setup中没有this, 所以通过bind来改变this的指向 const fn = storeStateFns[item].bind({$store: store}) //将最后的值放在storeState中 storeState[item] = computed(fn) }) return storeState } export default useState
测试hook
<hr>数组 <h2>{{counter}}</h2> <h2>{{name}}</h2> <h2>{{age}}</h2> <hr>对象 <h2>{{sAge}}</h2> <h2>{{sCounter}}</h2> <h2>{{sName}}</h2> <hr> setup() { const storeState = useState(["counter", "name", "age"]) const storeState2 = useState({ sCounter: state => state.counter, sName: state => state.name, sAge: (state) => state.age }) return { ...storeState, ...storeState2 } }
getters
有时候我们需要从 store 中的 state 中派生出一些状态。例如计算列表的长度,可以在多个地方进行复用。我们就可以在getters中定义,他的作用就好像计算属性computed。但是这个缓存好像有问题。
const store = createStore({ getters: { doubleRootCounter (state) { return state.age * 2 } } })
getters中定义的方法可以接受state作为一个参数
- 我们主要是为了处理state中的状态。
state () { return { counter: 100, name: "zh", age: 20, books: [ { name: "深入Vuejs", price: 200, count: 3 }, { name: "深入Webpack", price: 240, count: 5 }, { name: "深入React", price: 130, count: 1 }, { name: "深入Node", price: 220, count: 2 }, ], discount: 0.6, banners: [] }; }, getters: { currentDiscount(state) { return state.discount * 0.9 } }
getters中定义的方法也可以接受另一个参数getters
- 主要是为了结合其他的getter做一些事情。 下面这个例子是结合当前的折扣,来计算总价格
getters: { totalPrice(state, getters) { let totalPrice = 0 for (const book of state.books) { totalPrice += book.count * book.price } return totalPrice * getters.currentDiscount } }
如果我们想要使用外界传入的数据,来结合state中的状态,我们可以让getter返回一个函数,并将外界传入的数据作为这个函数的参数。
下面这个例子是让外界传入一个整数,来过滤计算的总价格
getters: { totalPriceCountGreaterN(state, getters) { return function(n) { let totalPrice = 0 for (const book of state.books) { if (book.count > n) { totalPrice += book.count * book.price } } return totalPrice * getters.currentDiscount } } }
我们定义了上面的getters方法,如何在项目中用起来呢?
- 在template中,直接通过
$store.getters
获取即可。
<h2>总价值: {{ $store.getters.totalPrice }}</h2> <h2>总价值: {{ $store.getters.totalPriceCountGreaterN(1) }}</h2>
- 在optionsAPI中,我们通过
this.$store.getters
获取即可。
computed: { totalPrice() { return this.$store.getters.totalPrice } }
- 在compositionAPI中,我们通过vuex提供的
useStore
API来获取。
setup() { const store = useStore() const sGetter = computed(() => store.getters.totalPrice) return { sGetter, } }
同获取state一样,如果我们有很多getter需要在store中获取,直接通过上述方法取值,比较繁琐,所以我们可以通过vuex提供的对应的map方法,mapGetters
。
他的使用同mapState一样,可以传入数组或者对象,在optionsAPI中的computed使用不会出现问题。
<h2>{{ sNameInfo }}</h2> <h2>{{ sAgeInfo }}</h2> <h2>{{ ageInfo }}</h2> <h2>{{ heightInfo }}</h2> getters: { nameInfo (state) { return `name: ${state.name}` }, ageInfo (state) { return `age: ${state.age}` } }, computed: { ...mapGetters(['nameInfo', 'ageInfo']), ...mapGetters({ sNameInfo: 'nameInfo', sAgeInfo: 'ageInfo', }), }
对应的我们可以借鉴对mapState封装的一个hook。我们只需要给mapState改成mapGetters即可。
import { useStore, mapGetters } from 'vuex' import { computed } from 'vue' const useGetters = (mapper) => { const store = useStore() const storeGetterFns = mapGetters(mapper) const storeGetter = {} Object.keys(storeGetterFns).forEach((item) => { const fn = storeGetterFns[item].bind({ $store: store }) storeGetter[item] = computed(fn) }) return storeGetter } export default useGetters
测试hook
<h2>{{ nameInfo }}</h2> <h2>{{ ageInfo }}</h2> setup() { const storeGetters = useGetters(['nameInfo', 'ageInfo']) return { ...storeGetters, } },