24Vue-Vuex-State和Mutations

215 阅读6分钟

序言

Vuex的五大核心

1.state

2.getters

3.mutations

4.actions:弥补mutations不能异步操作的功能

5.modules:对store状态划分模块

Vuex的状态管理

在开发中,我们的应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序中的某一个位置,对于这些数据的管理,我们称之为 “状态管理”

图片.png

data或setup中的数据:state

template模块中的数据最终被渲染成DOM:View

在模块中处理这些行为事件,会修改state:actions

复杂的状态管理(前面的是简单的状态管理)

图片.png

图片.png

把状态抽离出去,统一管理,这就是Vuex背后的思想

图片.png

State:存放各种数据

Vue Components:组件,任意组件都可以使用State数据,$store.state.name来使用

Mutations:Vuex不允许随便修改。我使用按钮点击来修改值,然后提交一个mutation-->commit mutation,来修改State中的值

可以直接从Vue Components组件里提交Mutations,跳过Actions。mutation:{函数1,函数2}

Mutation不允许有异步请求,而是在Actions中完成操作。

为什么Mutation不允许有异步请求?

同步:等到前面的任务执行完成后,我才能执行下一个任务,所以同步没有并行的概念

异步:两个任务可以同时执行,我可以边执行A也可以边执行B

我们使用devtools时,devtools可以帮助我们捕捉mutations的快照,但如果速速异步操作,那么devtools将不能很好的追踪这个操作什么时候会被完成。结果就会导致devtools记录的数据没有与页面显示的数据保持一致。

Vuex的具体演练

devtools方便我们对组件、vuex进行调试。他是chrome的一个插件

图片.png

图片.png

安装vuex

npm install vuex@next

store文件夹,专门存放vuex

store/index.js

//导入createStore函数
import {createStore} from 'vuex'

//调用该函数
const store = createStore({
    state(){
    //公共数据,存放在仓库里,任何组件都可以用
        return{
            counter:0
        }
    },
    mutations:{
    //这些函数被调用的时候会传来一个state
        increment(state){
            state.counter++
        }
    }
})

export default store

main.js

import store from './store'
createApp(App).use(store).mount('#app')

App.vue

使用仓库数据
<template>
    <h2>{{$store.state.counter}}</h2>
</template>
<script>
    export default{
        methods:{
            increment(){
            //这里要加 this,不允许在这里直接操作state里的数据
                this.$store.state.counter
            //而是commit提交mutation,由mutation修改State数据
            
            //commit("mutations中的方法名")
            this.$store.commit("increment")
            }
        }
    }
</script>

图片.png

当我App.vue中想要使用并修改state中数据的时候,我可以在仓库中建立一个mutations来存放想要使用发的方法,在这里我们就可以来修改state中的数据,完成相应的逻辑代码。然后再App.vue中的对应方法中,向仓库提交 想使用仓库的某个方法名,最后由仓库mutations的方法来完成操作。

单一状态树

图片.png

就是说:我可以创建多个const store1 = create{},然后在给他们一起export出去,但是vue不推荐我们这样。而是用一个对象包含全部的应用层级的状态。只会有一个store,但是给他模块化,所以不用担心混乱。

图片.png

useState

当我们通过{{$store.state.counter}}来拿仓库中的数据,在template中要写很长的表达式,我们可以通过计算属性方式来拿

{{sCounter}}

//通过hook拿到this.$store
import {mapState,useStore} from 'vuex'
import {computed} from 'vue'
    setup(){
        const store = useStore()
        const sCounter = computed(()=>{
            store.state.counter
        })
        return{
            sCounter
        }
    }
}

但是一旦数据多了,用计算属性还是会比较麻烦,此时我们可以使用辅助函数 mapState,对我们的State进行一个映射,本质还是通过计算属性来获得,但是要比我们直接对计算属性操作要方便点。只是换了个形式

vue2.0中的写法:
{{counter}}--{{name}}--{{age}}

export default{
    computed:{
        ...mapState(["counter","name","age"])
    }
//通过数组里传入的key来获取state中的value
//computed返回值是一个ref对象
//mapState是一个函数,里面的参数可以是数组也可以是对象。他的返回值也是一个对象

//它的内部实际上是这样的
    {
        counter:function(){},
        name:function(){}
    }
}
import {mapState,useStore} from 'vuex'
export default{
    setup(){
        const store = useStore()
        //01对象形式的函数,存在问题
        const storeState = mapState(["counter","name","age"])
        //{name:function,age:function}

        return{
        //02将storeState对象进行解构,然后再template中展示
        //实际上是对函数进行了展示,并不是我们想要的内容
            ...storeState
        }
      }
    }
}

如何解决返回的是对象里面存的是函数的问题?

Object.keys() 是用于获得由对象属性名组成的数组

Object.keys(person).forEach(function(trait) {
//trait:对象名,person[trait]:value
  console.log('Person ', trait,': ', person[trait]);
});
    让她被computed包裹即可,computed要求你传入一个函数,最终返回一个ref值
    {{counter}}
    setup(){
        const store = useStore()
        //内部存的形式是:{name:function,age:function}
        const storeStateFns = mapState(["counter","name","age"])
        
        const storeState = {}
        
        //通过遍历storeStateFns中的key得到里面的函数
        //并将函数赋值给fn,将获取到的fn被computed包裹
        //包裹后的生成了对象并被computed变成一个ref形式的
        //{name:ref,age:ref}响应式
        //赋值后生成的新对象,里面存放对应的value
        //Object.keys() 返回的正是一个对象属性[数组]
        
        Object.keys(storeStateFns).forEach(fnKey=>{
        //fnKey:是对象名key,storeStateFns[fnKey]:里面存的function也就是value
            const fn = storeStateFns[fnKey]
            //被computed包裹后返回一个对象,不再是函数
            //然后通过对象的key拿到相应的value的ref值
            storeState[fnKey] = computed(fn)
        })
        return{
            ...storeState
        }
    }

但是上方的代码仍然存在一个问题:不能读取$store属性

...mapState(["name","age"])计算属性帮我们读取数据的时候, 他会通过this.$store.state.name来读取,但是在setup中已经没有this指向了,const fn = storeStateFns[fnKey]里的function的this是一个undefined,所以在computed(fn)会报找不到$store的错误。

    const store = useStore()
    const storeStateFns = mapState(["counter","name","age"])
    const storeState={}
/*    
调用函数的bind方法,每个函数都有一个bind
里面要有属性,存放对象,这里的store是useStore的store
给storeStateFns[fnKey]绑定了一个this,赋值给了fn
当我们执行了storeStateFns[fnKey],他的this就是$store里的store
*/

        Object.keys(storeStateFns).forEach(fnKey=>{
            const fn = storeStateFns[fnKey].bind({$store:store})
            storeState[fnKey] = computed(fn)
        })
        return{
            ...storeState
        }

要么使用这种方法,要不然就老老实实的直接写const sCounter=computed(()=>store.state.counter)

图片.png 然后return出去,然后就可以直接在template中直接使用名字

封装

对刚刚的方法进行封装,这样以后使用的时候直接调用即可

hook就是函数,我们习惯将setup中需要封装的函数成为hook

src/hooks/useState.js

import {mapState,useStore} from 'vuex'
import {computed} from 'vue'

export function useState(mapper){
//拿到store独享
    const store = useStore()

//获取到响应对象的functions:{name:function,age:function}
    const storeStateFns = mapState(mapper)
    
//对数据进行转换,获取到ref的数据    
    const storeState = {}
    Object.keys(storeStateFns).forEach(fnKey=>{
        const fn = storeStateFns[fnKey].bind({$store:store})
        storeState[fnKey] = computed(fn)
    })
    
    return storeState
}

Home.vue使用hooks

<template>
    <div>
    {{counter}}--{{name}}--{{age}}
    {{sCounter}}--{{sName}}
    </div>
</template>

<script>
    import {mapState,useStore} from 'vuex'
    import {computed} from 'vue'
    import {useState} from '../hooks/useState'
    export default{
        setup(){
        //想用仓库里的什么,就在这里传什么
            const storeState = useState(["counter","name"])
        
        //传对象
        //可能我的setup中也有名字叫counter的
        这个时候使用对象的形式对state中也叫counter的取别名
            const storeState2 = useState({
                sCounter:state=>state.counter,
                sName:state=>state.name
            })
            
            return{
            //解构出来所有的属性
                ...storeState
                ...storeState2
            }
        }
    }
</script>

为啥我写了一个hook,居然直接支持对象和数组的两种形式呢?因为我实际上在hook中通过const storeStateFns = mapState(..)的mapState来处理,mapState本身就支持对象和数组的形式,所以无可厚非。

getters的基本使用

我想要得到每本书的销售额,如果和以前一样获取的话,需要在自己的页面,通过computed来获取。但是我们通过使用getters,对一些需要经过变化的数据来操作使用,以后我不管在哪个页面,都可以很方便的获取到。

store/index.js

import {createStore} from 'vuex'
const store = createStore(){
    state(){
        return{
            counter:100,
            name:"why",
            age:18,
            books:[
                {name:"1",price:200,count:3},
                {name:"2",price:220,count:1},
                {name:"3",price:240,count:4},
                {name:"4",price:250,count:6},
            ],
            discount:0.6,
        }
    },
    mutations:{},
    getters:{
    //当我的getters又依赖其他的getters,就可以使用他的第二个参数getters
        totalPrice(state,getters){
            let totalPrice = 0
            for(const book of state.books){
                totalPrice += book.count * book.price
            }
            //就可以通过getters参数取到getters中的其他
            return totalPrice * getters.currentDiscount
        },
        currentDiscount(state){
        //在之前的折扣里,又折上折
            return state.discount*0.9
        },
        
        //只计算数量大于n的书籍
        totalPriceCountGreaterN(state,getters){
            return function(n){
                let totalPrice = 0
                for(const book of state.books){
                    if(book.counter > n){
                    totalPrice += book.count * book.price
                    }
                }
                //对应 function 函数
                return totalPrice * getters.currentDiscount
            }
        }
    }
}

使用getters

//getters里是函数,但是里面会返回具体的值。展示返回值
{{$store.getters.totalPrice}}

//对getters里函数中又返回了一个函数并且是一个带参函数 的使用
{{$store.getters.totalPriceCountGreaterN(2)}}

如果不写()而是直接$store.getters.totalPriceCountGreaterN返回的实际上是一个函数,因为他内部就是return了一个函数,而加上了括号表示调用了这个函数,调用后会返回一个值,所以就可以正常展示数据

我们发现,在template中,书写getters的时候,表达式有点过长{{$store.getters.totalPriceCountGreaterN(2)}}为什么我们过分追究表达式过长这个问题?因为在{{}}的里面,我们不推荐过长的表达式,解决方案:

optional API的写法

{{nameInfo}}

export default{
    computed:{
        nameInfo(){
            return this.$store.getters.nameInfo
        }
    }
}

上面这种方式仍然很长,所以我们使用更优的:通过辅助函数

{{nameInfo}}--{{ageInfo}}
{{sNameInfo}}

import {mapGetters} from 'vuex'
export default{
    computed:{
    //传key,最终解构后得到value
        ...mapGetters(["nameInfo","ageInfo"])
        
    //对象的写法
        ...mapGetters({
        //直接写名称就行了,和mapState有所区别
            sNameInfo:"nameInfo"
        })
    }
}

composition API写法

<div>
    {{sNameInfo}}
</div>

import {computed} from 'vue'
import {mapGetters,useStore} from 'vuex'
export default{
    setup(){
        const store = useStore();
        
        //直接通过computed拿到
        const sNameInfo = computed(()=>{
          store.getters.nameInfo  
        })
        return{
          sNameInfo
        }
    }
}

使用Getters

先将实现Getters的方法抽取成一个hook,方便日后调用

/hooks/useGetters.js

import {mapGetters,useStore} from 'vuex'
import {computed} from 'vue'

export function useGetters(mapper){
//拿到store独享
    const store = useStore()

//获取到响应对象的functions:{name:function,age:function}
    const storeGettersFns = mapGetters(mapper)
    
//对数据进行转换,获取到ref的数据    
    const storeGetter = {}
    Object.keys(storeGettersFns).forEach(fnKey=>{
        const fn = storeGettersFns[fnKey].bind({$store:store})
        storeGetter[fnKey] = computed(fn)
    })
    
    return storeGetter
}
<div>
    {{sNameInfo}}
</div>

import {useGetters} from './hooks/useGetters'
export default{
    setup(){
        const storeGetters = useGetters(["nameInfo","ageInfo"]);
        return{
          ...storeGetters
        }
    }
}

大家发现,我的useGetters和useState的逻辑代码基本一样,只是一个用了mapGetters方法,一个用了mapState方法,但是我却要再写一遍,所以我们可以对这种代码再做一层封装。

/hooks/useMapper.js

import {mapGetters,useStore} from 'vuex'
import {computed} from 'vue'

//名字需要被修改
export function useMapper(mapper,mapFn){

    const store = useStore()

    //由传进来的mapFn来决定使用State还是Getters
    const storeMapperFns = mapFn(mapper)
        
    const storeMapper = {}
    Object.keys(storeMapperFns).forEach(fnKey=>{
        const fn = storeMapperFns[fnKey].bind({$store:store})
        storeMapper[fnKey] = computed(fn)
    })
    
    return storeMapper
}

由useState和useGetters来调用useMapper

/hooks/useState.js

import {mapState} from 'vuex'
import {useMapper} from './useMapper'

export function useState(mapper){
//调用useMapper并且返回结果
    return useMapper(mapper,mapState)
}

/hooks/useGetters.js

import {mapGetters} from 'vuex'
import {useMapper} from './useMapper'

export function useGetters(mapper){
//调用useMapper并且返回结果
    return useMapper(mapper,mapGetters)
}

其他页面正常引入useGetters和useState,然后将需要的值传进去,通过这两个方法再调用useMapper,最终得到结果

Mutation基本使用

我想通过鼠标点击btn然后修改store的值
就得通过提交,由vuex中的mutations中的increment来实现
<template>
    <button @click = "store.commit('increment')"></button>
</template>
state:{
    counter:100
},
mutations:{
    increment(state){
        state.counter++
    }
}

mutation中有两个参数,除了state还有一个payload,就是我从自己地方commit提交的时候,可以自己携带一个数据传入

Home.vue中
methods:{
    addTen(){
    //这里10就被提交后,变成了第二个参数payload
        this.$store.commit('incrementN',10)
        
    //真实开发中,我不可能只传一个数,如何解决传入多个数?
    //payload写成对象形式
        this.$store.commit('incrementN',{
            n:10,
            name:"why",
            age:20
        })
        
     //另外一种提交风格,是等价的
         this.$store.commit({
         //将提交的函数写成了type
            type:'incrementN',
            n:10,
            name:"why",
            age:20
        })
    }
}
在store/index.jsmutations:{
    incrementN(state,payload){
        state.counter += payload
    }
    //获取对象形式的payload,从对象里拿到具体属性
    incrementN(state,payload){
        state.counter += payload.n
    }
}
mutation辅助函数

我们每一次都需要$store.commit('..')来提交,我们又双叒叕嫌弃他太长了,我们就可以使用辅助函数了。

<template>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    //我像incrementN里面再传入一个参数
    <button @click="incrementN({n:10})">-1</button>
    <button @click="add">+1</button>
</template>
<script>
import {mapMutations} from 'vuex'
export default{
    methods:{
    //里面的参数都是mutations里的函数,就不用再写提交了
        ...mapMutations(["increment","decrement","incrementN"])
        
    //对象类型的写法
        ...mapMutations({
        //改变名字
            add:"increment"
        })
    }
}
</script>

以上全是在optional API中

下面是在composition API中书写

setup(){
    const storeMutations = mapMutations(["increment","decrement","incrementN"])
    
    //为什么他可以直接return解构呢?
    //因为我这个是被绑定在button的@click的
    它本身就要返回一个函数,我就是要函数,我就不用再把他转换成数据了
    return{
        ...storeMutations
    }
}