十一、Vue全家桶 – Vuex状态管理
Lyk 2022/8/30 Vuevuex
# 1、什么是状态管理
- 在开发中,我们会的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理我们就称之为是状态管理。
- 在前面我们是如何管理自己的状态呢?
- 在Vue开发中,我们使用组件化的开发方式;
- 而在组件中我们定义data或者在setup中返回使用的数据,这些数据我们称之为state;
- 在模块template中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View;
- 在模块中我们会产生一些行为事件,处理这些行为事件时,有可能会修改state,这些行为事件我们称之为actions;
# 2、复杂的状态管理
- JavaScript开发的应用程序,已经变得越来越复杂了:
- JavaScript需要管理的状态越来越多,越来越复杂;
- 这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等;
- 也包括一些UI的状态,比如某些元素是否被选中,是否显示加载动效,当前分页;
- 当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态;
- 来自不同视图的行为需要变更同一状态;
- 我们是否可以通过组件数据的传递来完成呢?
- 对于一些简单的状态,确实可以通过props的传递或者Provide的方式来共享状态;
- 但是对于复杂的状态管理来说,显然单纯通过传递和共享的方式是不足以解决问题的,比如兄弟组件如何共享数据呢?
# 3、Vuex的状态管理
管理不断变化的state本身是非常困难的:
- 状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
- 当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
因此,我们是否可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理呢?
在这种模式下,我们的组件树构成了一个巨大的 “视图View”;
不管在树的哪个位置,任何组件都能获取状态或者触发行为;
通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,我们的代码边会变得更加结构化和易于维护、跟踪(devtools);
这就是Vuex背后的基本思想,它借鉴了Flux、Redux、Elm(纯函数语言,redux有借鉴它的思想);
当然,目前Vue官方也在推荐使用Pinia进行状态管理,我们后续也会进行学习
# 4、vuex的Store对象
# 4.1.Vuex的安装
- 依然我们要使用vuex,首先第一步需要安装vuex:
- 我们这里使用的是vuex4.x;
## 安装状态管理
npm install vuex
1
2
2
# 4.2.创建Store
- 每一个Vuex应用的核心就是store(仓库):
- store本质上是一个容器,它包含着你的应用中大部分的状态(state);
- Vuex和单纯的全局对象有什么区别呢?
- 第一:Vuex的状态存储是响应式的
- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新;
- 第二:你不能直接改变store中的状态
- 改变store中的状态的唯一途径就显示提交 (commit) mutation;
- 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态;
- 第一:Vuex的状态存储是响应式的
- 使用步骤:
- 创建Store对象;
- 在app中通过插件安装;
# 4.3.组件中使用store
- 在组件中使用store,我们按照如下的方式:
- 在模板中使用;
- 在options api中使用,比如computed;
- 在setup中使用;compositon API中使用
- 以我们在store的state定义token状态为例:具体如下
<template>
<div class="home">
<h2>我是home组件</h2>
<p>{{$store.state.token}}</p> <!-- 1.template写法 -->
</div>
</template>
<script>
import { useStore } from 'vuex';
export default {
created() { // 2.Options API 写法
console.log(this.$store.state.token)
},
setup() { // 3.Composition API 写法
const store = useStore()
console.log(store.state.token)
}
}
</script>
<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 4.4.单一状态树
- Vuex 使用单一状态树:
- 用一个对象就包含了全部的应用层级的状态;
- 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源;
- 这也意味着,每个应用将仅仅包含一个 store 实例;
- 单状态树和模块化并不冲突,后面我们会讲到module的概念;
- 单一状态树的优势:
- 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难;
- 所以Vuex也使用了单一状态树来管理应用层级的全部状态;
- 单一状态树能够让我们最直接的方式找到某个状态的片段;
- 而且在之后的维护和调试过程中,也可以非常方便的管理和维护;
- vuex的三大核心:state,mutations,actions
# 5、vuex中store对象的状态state
# 5.1.组件获取状态及Options API 使用mapState辅助函数
- 在前面我们已经学习过如何在组件中获取状态了。
- 当然,如果觉得那种方式有点繁琐(表达式过长),我们可以使用计算属性:
<script>
export default {
computed:{
counter() {
return this.$store.state.count
}
}
}
</script>
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
- 但是,如果我们有很多个状态都需要获取话,可以使用mapState的辅助函数(映射):vuex 中辅助函数mapState的基本用法详解 (opens new window)
- mapState的方式一:对象类型;
- mapState的方式二:数组类型;
- 也可以使用展开运算符和来原有的computed混合在一起;
- mapState辅助函数-官方文档 (opens new window)
# 5.2.在setup中使用mapState辅助函数
- 在setup中如果我们单个获取装是非常简单的:
- 通过useStore拿到store后去获取某个状态即可;
- 但是如果我们需要使用 mapState 的功能呢?
- 具体写法如下图(推荐这种写法):该案例是mapState的传入数组类型写法;
- 当然也可以用传入对象类型写法具体参考上面5.1的 Options API 图片案例的 传入对象写法】
- 默认情况下,Vuex并没有提供非常方便的使用mapState的方式,这里我们也可以进行了一个函数的封装:
# 6、vuex中store对象的getters
# 6.1.getters的基本使用
- 某些属性我们可能需要经过变化后来使用,这个时候可以使用getters:
# 6.2.getters第二个参数
- getters可以接收第二个参数:getters 自己本身
- 第一个参数是:state状态
import { createStore } from 'vuex'
export default createStore({
state(){
return {
//...
}
},
getters:{
totalPrice(state,getters) {
const total_price = state.books.reduce((previousValue,currentValue) => {
return previousValue + currentValue.count * currentValue.price
},0)
return total_price + "," + getter.myName
},
myName(state,getters) {
return state.name
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 6.3.getters的返回函数写法
- getters中的函数本身,也可以返回一个函数,那么在使用的地方相当于可以调用这个函数(跟计算属性类似):(调用时可以传入参数)
import { createStore } from 'vuex'
export default createStore({
state(){
return {
books:[
{name:'你不知道的Javascript',count:2,price:50},
{name:'Javascript高级程序设计',count:6,price:100},
{name:'追风筝的人',count:10,price:52},
]
//...
}
},
getters:{
totalPrice(state,getters) {//getters中的函数本身,可以返回一个函数,那么在使用的地方相当于可以调用这个函数:
return price => {
const total_price = state.books.reduce((previousValue,currentValue) => {
if( currentValue.price < price) return previousValue //书籍价格小于price的不列入求和计算
return previousValue + currentValue.count * currentValue.price
},0)
return total_price
}
},
}
})
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
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
- 以上面store中的数据为例,我们下面进行使用
<template>
<p>仓库中价格大于等于60的书籍一共可以卖{{$store.getters.totalPrice(60)}}元</p>
<!-- 你会看到浏览器显示:仓库中价格大于60的书籍一共可以卖600元 -->
</template>
1
2
3
4
2
3
4
# 6.4.mapGetters的辅助函数
mapGetters辅助函数 (opens new window)
以下代码案例 借用上面 6.2和6.3 定义的数据为基础
Options API 使用mapGetters的辅助函数。
<templte>
<h2>{{ finalPrice }}</h2>
<h2>{{ finalName }}</h2>
</templte>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
//...mapGetters['totalPrice','myName'],
...mapGetters[{//如果你想将一个 getter 属性另取一个名字,使用对象形式:
finalPrice:'totalPrice',
finalName:'myName
}]
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- Composition API 的setup中使用
<templte>
<h2>{{ cTotalPrice }}</h2>
<h2>{{ cMyName }}</h2>
</templte>
<script setup>
import { useStore,mapGetters } from 'vuex'
import { computed } from 'vue'
const store = useStore
const {totalPrice,myName} = mapGetters(['totalPrice','myName'])
//如果你想将一个 getter 属性另取一个名字,使用对象形式:
//const {totalPriceName} = mapGetters({
// totalPriceName:"totalPrice"
//})
const cTotalPrice = computed(totalPrice.bind({$store:store}))
const cMyName = computed(myName.bind({$store:store}))
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 7、vuex中store对象的Mutations
# 7.1.Mutation基本使用
- 更改 Vuex 的 store 中的状态(state)的唯一方法是提交 mutation:(这是vuex状态管理库约定熟成的规范)
import { createStore } from 'vuex'
export default createStore({
state(){
return{
counter:0
}
},
mutations:{
increment(state){
state.counter++
},
decrement(state){
state.counter--
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 7.2.Mutation携带数据
- 很多时候我们在提交mutation的时候,会携带一些数据,这个时候我们可以使用参数:
import { createStore } from 'vuex'
export default createStore({
state(){
return{
counter:0
}
},
mutations:{
addNumber(state,payload) {//如这里payload接收到的参数为:{count:10}
state.counter += payload.count
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 提交方式:$store.commit
- 通过store实例对象的commit提交方法时,可以携带任何类型参数;
<template>
<h2>counter:{{$store.state.counter}}</h2>
<button @click='changeCounter(10)'>点击counter+10</button>
</template>
<script setup>
import { useStore } from 'vuex'
const store = useStore()
function changeCounter(count) {
store.commit('addNumber',{count})
//store.commit({//对象风格提交方式
// type:'addNumber',
// count
//})
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 7.3.Mutation常量类型
# 7.4.mapMutations辅助函数
以下代码案例 借用上面7.3 定义的常量及mutations 数据为基础
我们也可以借助于辅助函数,帮助我们快速映射到对应的方法中:Options API
<template>
<!-- 在methods中定义方法,然后通过this进行使用 -->
<button @click="changeCounter(10)">counter+10</button>
<!-- mutations对象通过展开辅助函数,可以直接进行使用 -->
<button @click="Add_NUMBER_YS(10)">counter+10</button>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
methods:{
//...mapMutations(['ADD_NUMBER']),
...mapMutations({//将mutations里的方法映射到该组件内
ADD_NUMBER_YS:'ADD_NUMBER'
}),
changeCounter(count) {
//this.ADD_NUMBER({count})
this.Add_NUMBER_YS({count})////由于上一步已经将mutation映射到组件内,所以组件可以直接调用Add_NUMBER_YS
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 在setup中使用也是一样的:Composition API
<templte>
<h2>counter:{{$store.state.counter}}</h2>
<button @click='changeCounter(10)'>点击counter+10</button>
</templte>
<script setup>
import { useStore,mapMutations } from 'vuex'
const store = useStore
const { ADD_NUMBER } = mapMutations(['ADD_NUMBER'])
//如果你想将一个 mutations 方法另取一个名字,使用对象形式:
//const { ADD_NUMBER_YS } = mapMutations({
// ADD_NUMBER_YS:"ADD_NUMBER"
//})
function changeCounter(count) {
ADD_NUMBER.bind({$store:store})({count})
//ADD_NUMBER_YS.bind({$store:store})({count})
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.5.mutation重要原则
- 一条重要的原则就是要记住 mutation 必须是同步函数(mutations中只能处理同步操作)
- 这是因为devtool工具会记录mutation的日记(快照);
- 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
- 但是在mutation中执行异步操作,就无法追踪到数据的变化;
- 所以Vuex的重要原则中要求 mutation必须是同步函数;
- 但是如果我们希望在Vuex中发送网络请求的话需要如何操作呢? actions
# 8、vuex中store对象的actions
# 8.1.actions的基本使用
- Action类似于mutation,不同在于:
- Action提交的是mutation,而不是直接变更状态;
- Action可以包含任意异步操作;
- 这里有一个非常重要的参数context:
- context是一个和store实例均有相同方法和属性的context对象;
- 所以我们可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;
- 但是为什么它不是store对象呢?这个等到我们学到Modules时再具体展开来说
import { createStore } from 'vuex'
export default createStore({
state(){
return{
counter:0
}
},
mutations:{
incrementMutation(state,payload) {
state.counter += payload
}
},
actions:{
increment(context,payload) {
//console.log(context.state,context.getters)
context.commit('incrementMutation',payload)
}
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8.2.actions的分发操作
- 如何使用action呢?进行action的分发:
- 分发使用的是 store 上的dispatch函数;
- 同样的,它也可以携带我们的参数:
- 也可以以对象的形式进行分发:
【以下代码案例 借用上面8.1定义的数据为基础】
<template>
<h2>counter:{{$store.state.counter}}</h2>
<button @click="incrementCounter(1)">counter+1</button>
</template>
<script>
export default {
methods:{
incrementCounter(num) {
//this.$store.dispatch("increment",num)// 同样的,它也可以携带我们的参数(任意类型都可以):如这里的num
this.$store.dispatch({//也可以以对象的形式进行分发:
type:'increment',
num
})
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 8.3.actions的辅助函数
- action也有对应的辅助函数:mapActions
- 对象类型的写法;
- 数组类型的写法;
- 【以下代码案例 借用上面8.1定义的数据为基础】
- Options API 使用mapActions辅助函数
<template>
<div class="actions">
<h2>我是Actions组件</h2>
<h2>counter:{{$store.state.counter}}</h2>
<!-- 在methods中定义方法,然后通过this进行使用 -->
<button @click="increment(2)">counter+2</button>
<!-- actions对象通过展开辅助函数,可以直接进行使用 -->
<button @click="incrementThree(3)">counter+3</button>
<button @click="incrementAction(6)">counter+6</button>
</div>
</template>
<script>
import { mapActions } from 'vuex';
export default {
methods:{
...mapActions(['incrementAction']),
...mapActions({//如果你想将一个 actions 方法另取一个名字,使用对象形式:
incrementThree:'incrementAction'
}),
increment(num) {
this.incrementAction(num)
}
}
}
</script>
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
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
- Composition API 的 setup 函数中使用mapActions辅助函数
<template>
<div class="actions">
<h2>我是Actions组件</h2>
<h2>counter:{{$store.state.counter}}</h2>
<button @click="incrementSetup(20)">counter+20</button>
<button @click="incrementThreeSetup(30)">counter+30</button>
</div>
</template>
<script setup>
import { mapActions, useStore } from 'vuex';
const store = useStore()
//const { incrementAction } = mapActions(['incrementAction'])
const action1 = mapActions(['incrementAction'])
//如果你想将一个 actions 方法另取一个名字,使用对象形式:
const { incrementThreeSetupName } = mapActions({
incrementThreeSetupName:'incrementAction'
})
function incrementSetup(num) {
// console.log(incrementAction)
// incrementAction.bind({$store:store})(num)
console.log(action1)
action1.incrementAction.bind({$store:store})(num)
}
function incrementThreeSetup(num) {
incrementThreeSetupName.bind({$store:store})(num)
}
</script>
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
31
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
31
# 8.4.actions的异步操作
- Action 通常是异步的,那么如何知道 action 什么时候结束呢?
- 我们可以通过让action返回Promise,在Promise的then中来处理完成后的操作;
# 9、vuex中store对象的modules
# 9.1.module的基本使用
- 什么是Module?
- 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿;
- 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module);
- 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块modules;
# 9.2.module的局部状态
- 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态state对象
import { createStore } from 'vuex'
const ADD_NUMBER = 'ADD_NUMBER'
const moduleA = {
state(){//局部模块状态
return{
address:'洛杉矶',
name:'rose'
}
},
getters:{
addressName(state,getters,rootState){//rootState是根部的状态对象
return state.address + '-' + state.name + rootState.count //这里返回的是:洛杉矶=rose520
}
},
mutations:{
changeAddressMutation(state,payload) {
state.address = payload
}
},
actions:{
changeAddressAction(context,payload) {
context.commit('changeAddressMutation',payload)
},
//changeAddressAction({commit,rootState},payload) {
// commit('changeAddressMutation',payload)
// console.log(rootState.count)//520
//}
}
}
export default createStore({
state(){//根状态
return {
count:520
}
}
modules: {
Submodule:moduleA
}
})
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
31
32
33
34
35
36
37
38
39
40
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
31
32
33
34
35
36
37
38
39
40
# 9.3.module的命名空间
- 默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的:
- 这样使得多个模块能够对同一个 action 或 mutation 作出响应;
- Getter 同样也默认注册在全局命名空间;
- 如果我们希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:
- 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;
# 9.4.module修改或派发根组件
- 如果我们希望在局部模块action中修改root中的state,那么有如下的方式:
- 若需要在全局命名空间内分发 action 或提交 mutation,将
{ root: true }
作为第三参数传给dispatch
或commit
即可。
- 若需要在全局命名空间内分发 action 或提交 mutation,将
- Modules.vue
<template>
<div class="modules">
<h2>我是Modules组件</h2>
<!-- <h2>addressName:{{$store.getters.addressName}}</h2> --><!-- 子模块未设置了:namespaced:true的情况 -->
<h2>addressName:{{$store.getters['Submodule/addressName']}}</h2><!-- 子模块设置了:namespaced:true的情况 -->
<h2>address:{{$store.state.Submodule.address}}</h2>
<button @click="changeAddress('上海')">修改Submodule模块的address属性</button>
<button @click="changeAddress1('北京')">修改Submodule模块的address属性</button>
<!-- 根组件数据展示 -->
<h2>rootAddress:{{$store.state.counter}}</h2>
</div>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore()
//通过 store.state.Submodule 可以拿到子模块Submodule的状态
console.log(store.state.Submodule.name)//rose
function changeAddress(address) { //提交子模块Submodule的mutation
// store.commit('changeAddressMutation', address) //子模块未设置了:namespaced:true的情况
store.commit('Submodule/changeAddressMutation', address) //子模块设置了:namespaced:true的情况
}
function changeAddress1(address) { //分发子模块Submodule的action
// store.dispatch('changeAddressAction',address) //子模块未设置了:namespaced:true的情况
store.dispatch('Submodule/changeAddressAction',address) //子模块设置了:namespaced:true的情况
}
</script>
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
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
- store/index.js
import { createStore } from 'vuex'
const moduleA = {
namespaced:true,//使该模块成为带命名空间的模块
state(){
return{
address:'洛杉矶',
name:'rose'
}
},
getters:{
addressName(state,getters,rootState,rootGetters){
return state.address + '-' + state.name + '-' + rootState.counter
}
},
mutations:{
changeAddressMutation(state,payload) {
state.address = payload
}
},
actions:{
changeAddressAction({rootState,commit,dispatch},payload) {
console.log('局部模块状态2:',rootState.token)
commit('changeAddressMutation',payload)
//如果我们希望在局部模块action中修改root中的state,那么有如下的方式:
commit('addNumber',{num:11},{root:true})//在子模块 分发 全局的mutations
dispatch('incrementAction',52,{root:true})//在子模块 分发 全局的actions
}
//若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
export default createStore({
state: {
counter:520
},
mutations: {
addNumber(state,payload) {
state.counter += payload.num
},
incrementMutation(state,payload) {
state.counter += payload
console.log(`counter+${payload}成功`)
}
},
actions: {
incrementAction(context,payload) {
console.log('first')
return new Promise((resolve,reject) => {
setTimeout(() => {
context.commit('incrementMutation',payload)
resolve('incrementAction执行成功')
},1000)
})
}
},
modules: {
Submodule:moduleA
}
})
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67