状态管理模式Vuex | 我的日常分享

状态管理模式Vuex

状态管理模式Vuex

一、工作流程

image-20220815040131415

组件(例如用户点击按钮)通过Dispatch触发Actions,Actions通过Commit触发MutatiosMutations改变State,伴随着State的改变重新渲染组件。

用户可以直接操作Mutatios吗?答案是可以的,Mutations支持同步操作,Actions支持异步操作。

vuex

二、State

在 Vue 组件中获得 Vuex 状态

1、通过store实例直接获取

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

1
2
3
4
5
6
7
8
9
import store from '@/store/index'
// 获取store中定义的购物车商品数据
export default {
computed: {
goods () {
return store.state.cart.goods
}
}
}

上面的方式在模块化开发中,需要在需要使用store的组件中频繁地导入store

Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过 this.$store 访问到。于是上面的方式可以改写为:

1
2
3
4
5
6
7
8
// 获取store中定义的购物车商品数据
export default {
computed: {
goods () {
return this.$store.state.cart.goods
}
}
}

2、mapState 辅助函数获取state

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
data () {
return {
str: 'hello'
}
},
computed: mapState({
// 箭头函数可使代码更简练
goods: store => store.cart.goods,
// 传字符串等效于 store => store.cart
cart: 'cart',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
mixinStr (store) {
return store.cart.totalPrice + this.str
}
})
}

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

1
2
3
4
 computed: mapState(
// 等同于cart: store => store.cart
['cart']
)

3、mapState结合对象展开运算符

mapState 函数返回的是一个对象,通过使用对象展开运算符,我们就可以在使用mapState的同时定义其它的计算属性值了。

1
2
3
4
5
6
7
8
computed: {
...mapState({
goods: store => store.cart.goods
}),
otherComputed () {
return '我是其它的计算属性'
}
}

我们可以把mapState的返回值打印出来看看:

1
2
3
4
5
6
7
8
created () {
const mapState1 = mapState({
goods: store => store.cart.goods,
totalPrice: 'totalPrice'
})
console.log(mapState1)
console.log(mapState1.goods)
}

image-20220815055042134

三、Getter

为什么会有Getter?有时候我们需要从store中派生出一些状态,例如购物车总金额、或者对列表进行过滤计数等等。

如果在不同的.vue中的多个组件需要使用到该状态,那我们就需要把Computed中写的购物车总金额、列表过滤的计算属性完整的复制到每一个需要使用到该状态的.vue中,或者抽取到一个公共的函数中对它进行引用–无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。我们可以在getter中对状态进行处理返回。

可以类比Java中的get方法。

Getter 接受 state 作为其第一个参数(自动传入该参数):

我们可以通过state参数获取到状态,对其进行处理再进行返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = createStore({
state: {
todos: [
{ id: 1, text: '123', done: true },
{ id: 2, text: '456', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})

1、通过属性访问(getter直接返回处理后的state)

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值。

例如:返回商品价格大于6000的商品。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export default new Vuex.Store({
state: {
cart: {
goods: [
{
id: 1,
name: 'iPhone 13 Pro',
desc: 'iPhone 13 Pro 大步飞跃,做什么都速度惊人,拍什么都表现惊艳,更有两款尺寸,称心称手。',
price: 7999,
num: 0,
img: require('@/assets/iphone-13-pro.jpeg')
}
],
totalNum: 0,
totalPrice: 0
}
},
getters: {
// 获取价格大于6000的商品
priceGt6000 (state) {
return state.cart.goods.filter((item) => item.price > 6000)
}
},
mutations: {
},
actions: {
},
modules: {
}
})

Getter 也可以接受其他 getter 作为第二个参数:也就是在当前getter中调用定义的其它的getter方法

1
2
3
4
5
6
getters: {
// 获取价格大于6000的商品
priceGt6000 (state) {
return state.cart.goods.filter((item) => item.price > 6000)
}
}

我们可以在任何组件中很容易的通过像使用state一样使用它:

1
2
3
4
5
computed: {
priceGt6000Goods () {
return this.$store.getters.priceGt6000
}
}

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

2、通过方法访问(getter返回的是函数)

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。

1
2
3
4
5
6
7
8
9
10
11
12
getters: {
// 根据id获取goods
getGoodsById (state) {
return function (id) {
return state.cart.goods.filter((item) => item.id === id)
}
},
// 上面的方式可以改造成箭头函数
getGoodsById1: (state) => (id) => {
return state.cart.goods.filter((item) => item.id === id)
}
},

在组件中进行使用:

1
this.$store.getters.getGoodsById(1)
1
2
3
created () {
console.log(this.$store.getters.getGoodsById(1))
},

image-20220815110245070

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

3、mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,与mapState类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { mapGetters } from 'vuex'

export default {
// ...
computed: {
...mapGetters([
// 使用对象展开运算符将 getter 混入 computed 对象中
'priceGt6000',
'priceGt6000Count',
'getGoodsById'
]),
// getter 属性另取一个名字,使用对象形式
...mapGetters({
// 把 `this.getGoodsById` 映射为 `this.$store.getters.getGoodsById`
getGoodsByIdAlias: 'getGoodsById'
})
}
}

如果你想将一个 getter 属性另取一个名字,使用对象形式:

1
2
3
4
5
// getter 属性另取一个名字,使用对象形式
...mapGetters({
// 把 `this.getGoodsById` 映射为 `this.$store.getters.getGoodsById`
getGoodsByIdAlias: 'getGoodsById'
})

调用:

1
2
3
4
5
6
created () {
console.log(this.priceGt6000)
console.log(this.priceGt6000Count)
console.log(this.getGoodsById(2))
console.log(this.getGoodsByIdAlias(3))
}

image-20220815111418570

四、Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。

4.1 不带载荷的mutation

例如:定义一个事件类型typechangePrice,回调函数handler内容为state.cart.totalPrice = 100mutation

1
2
3
4
5
6
7
8
9
10
11
export default new Vuex.Store({
state: {
cart: {
... ...
},
mutations: {
changePrice (state) {
state.cart.totalPrice = 100
}
}
})

你不能直接调用一个 mutation 处理函数。即如下:

1
this.$store.mutations.changePrice()

mutations更像是事件注册:“当触发一个类型为 changePrice 的 mutation 时,调用此函数。”

要使用一个 mutation 处理函数,需要用相应的 type 调用 store.commit 方法:

1
this.$store.commit('changePrice')

4.2 带载荷的mutation

可以向 store.commit 传入额外的参数,以传入需要的数据,即 mutation 的载荷(payload)

1
2
3
4
5
6
7
8
9
10
11
export default new Vuex.Store({
state: {
cart: {
... ...
},
mutations: {
changePriceA (state, a) {
state.cart.totalPrice = a
}
}
})

调用mutation

1
this.$store.commit('changePriceA', 500)

在大多数情况下,载荷通常是以对象形式传入的,这样可以包含多个字段并且记录(dev-tool工具会记录到,本文后面会讲到)的 mutation 会更易读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default new Vuex.Store({
state: {
cart: {
... ...
},
mutations: {
changePriceB (state, payload) {
state.cart.totalPrice = payload.price
},
// 也可以采用解构的方式
changePriceB (state, { price }) {
state.cart.totalPrice = price
}
}
})

调用mutation

1
2
3
this.$store.commit('changePriceB', {
price: 700
})

4.3 对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

1
2
3
4
this.$store.commit({
type: 'changePriceB',
price: 800
})

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变

1
2
3
4
5
mutations: {
changePriceB (state, payload) {
state.cart.totalPrice = payload.price
}
}

分别使用两种风格提交mutation,查看打印出来的载荷.

1
2
3
4
5
6
mutations: {
changePriceB (state, payload) {
console.log(payload)
state.cart.totalPrice = payload.price
}
}
1
2
3
4
5
6
7
8
    
this.$store.commit('changePriceB', {
price: 700
})
this.$store.commit({
type: 'changePriceB',
price: 800
})

结果:可以看到以对象风格提交的数据,载荷是包含type的对象,所以接受数据时要注意使用解构或使用payload.的方式获取到数据。

image-20220828232625502

4.4 mapMutations

在组件中可以使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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')`
})
}
}

注意:mapMutations需要使用在methods内,而不是computed,因为mapMutations返回的是mutation事件函数,说到这那为什么mapGetters返回的函数可以在computed中定义呢?那我们直接把东西打印出来看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
craeted(){
console.log(this.getGoodsById)
console.log(this.changePriceB)
},
computed{
...mapGetters([
'getGoodsById'
]),
},
methods: {
...mapMutations([
'changePriceB'
])
},

image-20220828234728812

如果将mapMutations定义在computed内:console.log(this.changePriceB)的输出值为undefined

image-20220828235353900

4.5 Mutation 必须是同步函数

Mutation的回调函数handler中的内容必须是同步的,否则其中异步改变的状态将不会被devtool 中的 mutation 日志记录到。

例如:下面定义的testMutation中包含一个axios异步请求与setTimeout定时器,其中将state.count的值进行了加1,这两个加1的操作将不会被devtool的日志记录器锁记录到。

1
2
3
4
5
6
7
8
9
10
11
mutations: {
testMutation (state) {
state.count++
axios.get('', () => {
state.count++
})
setTimeout(() => {
state.count++
}, 3000)
}
}

Vuex官方解释:

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

1
2
3
4
5
6
7
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

4.6 如果需求包含异步操作,而Mutation要求是同步的?

Vuex为我们提供了Action,在Action中可以进行异步操作。

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务

1
2
store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。否则不会被调试器记录到

4.7 使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

1
2
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能
// 来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})

用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。

五、Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

取state为什么要使用computed?

为什么Mutation是异步的,action可以异步以及同步?

vue-devtools

通过this.$store.state.xxx能够对状态进行获取以及修改,那么getter、Mutation、action的存在的意义是什么?

mixins混入 p120

vue持久化问题