25Vue-Vuex(action-module)

543 阅读5分钟

actions的基本使用

当我们想把异步操作,移动到vuex中时,但是mutations并不能帮你解决异步操作。所以vuex帮我们多增加了一层:Actions层。通过actions中,完成异步操作,然后再在actions中commit提交给Mutations中,然后再通过Mutations来修改State中的数据。

图片.png

index.js

组件先使用actions,然后通过actions自己调用了mutations,最后mutations来修改state

state(){
    counter:100
},
actions:{
    //放函数
    incrementAction(context){
    //通过context来提交给Mutations
        context.commit('increment')
    }
},
mutations:{
    increment(state){
        state.counter++;
    }
},
Home.vue
<template>
    <div>
        <button @click="increment">+1</button>
    </div>
</template>

<script>
    export default{
        methods:{
            increment(){
            //commit提交 是mutations
            //这里 dispatch分发 是actions
                this.$store.dispatch("incrementAction")
            },
            setup(){
            
            }
        }
    }
</script>
运行结果

demo08.gif

执行异步操作(使用actions)

上面的例子并不能看出actions的作用。下面举例:

我希望我点击按钮的时候,延迟一秒+1

index.js
state(){
    counter:100
},
actions:{
    incrementAction(context){
    //这里进行异步操作
        setTimeout(()=>{
            context.commit('increment')
        },1000)
    }
},
mutations:{
    increment(state){
        state.counter++;
    }
},
运行结果

demo08.gif

异步操作高级应用

使用axois的前提:安装他npm install axois

Home.vue

<template>
    <div>
        <button @click="increment">+1</button>
    </div>
</template>

<script>
//引入axois
import axios from 'axios'
    export default{
        methods:{
            increment(){
                 this.$store.dispatch("incrementAction")
            },
        },
        //我们不希望一些请求在组件里完成,而是在vuex中
        //所以这里网络请求dispatch分发给actions处理
        mounted(){   
            this.$store.dispatch("getHomeMuldata",res.data.banner.list)
        },
      }
    }
</script>

index.js

state(){
    counter:100,
    //网络请求后得到的数据,初始化
    banners:[]
},
actions:{
    incrementAction(context){
        setTimeout(()=>{
            context.commit('increment')
        },1000)
    },
    getHomeMuldata(context){
        //在这里进行网络请求,get返回一个promise
        axois.get("服务器地址").then(res=>{
            this.$store.commit("addBannerdata",res.data.banner.list)
        }    
    }
},
mutations:{
    increment(state){
        state.counter++;
    },
    //操作网络请求后得到的轮播图数据
    addBannerData(state,payload){
    //将网络请求得来的数据存到仓库banners中
        state.banners = payload
    }
},

请求后得到的数据(服务器里的数据):

图片.png

运行结果:(服务器里的banner.list存入仓库中)

图片.png

actions参数

Home.vue

methods:{
    increment(){
    //除了分发给mutations的函数外,自己还传了一个对象
        this.$store.dispatch("incrementAction",{
            count:100,
        })
    }
}

index.js

actions:{
//自己书写的对象参数,actions中 payload接收
    incrementAction(context,payload){
        console.log(payload)//{count:100}
        setTimeout(()=>{
            context.commit('increment')
        },1000)
        console.log(context)
    }
},
actions中的context参数存在的方法:

图片.png

commit方法:很明显就是从我actions的context里面,拿到commit方法,提交给mutation

dispatch方法:我可能会在我这个action中调用actions中的其他action
这个时候就可以用context.dispacth("action")

getters方法:在actions中获取到getters里的info属性
context.getters.info

rootGetters方法:分模块才会使用到
rootState方法:分模块才会使用到

state方法:通过context.state.拿到state中具体的数值
分发的第二种写法

Home.vue

methods:{
    decrement(){
        this.$store.dispatch({
            //分发的方法也可以写成对象形式
            //这里写的就是分发给actions中的函数
            type:"decrementAction"
        })
    }
}

actions中的辅助函数(optional API)

Home.vue

<template>
    <button @click="incrementAction">+1</button>
    
    <button @click="add">+1</button>
</template>

<script>
    import {mapActions} from 'vuex'
    export default:{
        methods:{
        //不用再书写一个方法,然后里面还要dispatch
        //直接利用辅助函数分发给action
     数组写法    ...mapActions(["incrementAction"])
     对象写法    ...mapActions({
         //对这个方法重新命名了
             add:"incrementAction"
         })
     }
    }
</script>

actions中的辅助函数(composition API)

Home.vue

<template>
    <button @click="incrementAction">+1</button>
    
    <button @click="add">+1</button>
</template>

<script>
    import {mapActions} from 'vuex'
    export default{
        setup(){
            //数组
            const actions = mapActions(["incrementAction"])
            
            //对象
            const actions2 = mapActions({
                add:"incrementAction"
            })
            
            //解构
            return{
                ...actions,
                ...actions2
            }
        }
    }
</script>

因为actions中存储的本来就是方法,所以不需要用computed包裹成一个ref对象

Composition API使用actions

Home.vue

<template>
    <div></div>
</template>

<script>
    import {onMounted} from 'vue'
    import {useStore} from 'vuex'
    export default{
        setup(){
        //因为mounted不是Composition API
        //所以这里使用onMounted
            onMounted(()=>{
            //通过引入usestore才能拿到store
                const store = useStore()
                
                onMounted(()=>{
                    store.dispatch("getHomeMuldata")
                })
            })
        }
    }
</script>

index.js(和上方的index.js一样保持不变)

actions:{
    getHomeMuldata(context){
        axois.get("服务器地址").then(res=>{
            this.$store.commit("addBannerdata",res.data.banner.list)
        }    
    }
},

actions细节补充

actions中是没有返回值的,因为它里面存储的都是函数,返回值其实是一个undefined

但是我actions中的每个函数是可以有一个Promise()的返回值的。return new Promise()

这意味着,我在组件里dispatch的时候可以接收返回的值 const promise = store.dispatch("increment")

此时就可以监听promise的then,知道什么内部时候执行结束,也可以传输数据,也可以拿到你的错误信息

Home.vue

setup(){
    const store = useStore()
    
    onMounted(()=>{
    //获取到getHomeMuldata返回的结果值
        const promise = store.dispatch("getHomeMuldata")
    //如果我的返回结果是true,我就输出promise中正确的信息    
        promise.then(res=>{
            console.log(res);//成功啦美汁汁
        }).catch(err=>{
        //返回结果报错了我就捕获他,打印错误信息
            console.log(err)
        })
    })
}

index.js

actions:{
    getHomeMuldata(context){
        return new Promise((resolve,reject)=>{
        //这里是resolve成功的结果
            axois.get("服务器地址").then(res=>{
                this.$store.commit("addBannerdata",res.data.banner.list)
                //成功的话,我就回调Home.vue中的then
                resolve("成功啦美汁汁")
            }).catch(err=>{
                reject(err)
               } 
            })
        })
    }
}

模块modules的使用

如果我的项目非常的庞大,有很多的功能。vuex作为一个仓库,公共的数据全部存储在其中。但是这样还是不够完善。

我在设计首页Home的时候,Home组件里面一定会再包含着子组件,子组件里可能还会包含子组件,当我的其他页面比如 登录页面、商品页面肯定也会有很多子组件。

当我把这些大组件的所有公共的数据全部存储在一个vuex中,就会造成数据使用的错误(如果存在同名的情况,举个例子而已)

我们希望所有和Home组件有关的子组件,他们使用的公共数据存储在一块;和登录大组件有关的一些若干子组件,他们使用的公共数据又存储在其他地方。

模块里面又可以分为若干个子模块,不过实际上一个vuex里分为若干模块就可以了,再在若干模块里面又分为若干子模块就有点麻烦了。

但是vue官方并不推荐我们创建多个store对象,这个时候就需要使用 modules分模块 进行对相同类型组件的公共数据的管理了。

图片.png

模块的演练

模块的本质就是一个对象

export default ____ 使用default,后续引用的时候,可以不加 大括号{}

直接export ___,引用的时候需要加上大括号{}。

export default暴露的成员可以用任意变量来接收,所以下方我import进来的名字可以随便取,不用和export出去的名字相同

src/modules/home.js

const homeModule = {
    state(){
        return{
            homeCounter:100
        }
    },
    getters:{
    
    },
    mutations:{
    
    },
    actions:{
    
    },
}
export default homeModule

src/modules/user.js

const userModule = {
    state(){
        return{
            userCounter:10
        }
    },
    getters:{
    
    },
    mutations:{
    
    },
    actions:{
    
    },
}
export default userModule

index.js 最大的公共仓库

import {createStore} from 'vuex'
import home from './modules/home'
import user from './modules/user'

const store = createStore({
    state(){
        return{
            rootCounter:0
        }
    },
    mutations:{
        increment(state){
            state.rootCounter++
        }
    },
    //必须还要在大的vuex仓库中,进行modules声明
    modules:{
    //key:value,如果key和value可以简写
        home:home 
        user
    }
})

export default store

使用vuex

Home.vue

<template>
    <div>
    //如果这样写的话,取到的肯定是根的
    {{$store.state}}
    
    //根据modules中的key来取,这样写是因为这样设计的
    {{$store.state.home.homeCounter}}
    {{$store.state.user.userCounter}}
    </div>
</template>

<script>
    import 
    export default{
        setup(){
            
        }
    }
</script>

module的局部状态

图片.png

src/modules/home.js

const homeModule = {
    state(){
        return{
            homeCounter:100
        }
    },
    getters:{
        doubleHomeCounter{
            return homeCounter*2
        }
    },
    mutations:{
        increment(state){
            state.homeCounter++
        }
    },
    actions:{
        incrementAction(context){
            context.commit("increment")
        }
    },
}
export default homeModule

Home.vue

<template>
    <div>
        <button @click="homeIncrement">home+1</button>
        
        //取home.js中的getters
        //这个getters中的方法在哪个模块中没有明确指出,所以存在隐患
        {{$store.getters.doubleHomeCounter}}
        
        <button @click="homeIncrementAction">
        </button>
    </div>
</template>

<script>
    import 
    export default{
        methods{
            homeIncrement(){
            //使用home.js模块里的mutations中的方法
                this.$store.commit("increment")
            },
            homeIncrementAction(){
                this.$store.dispatch("incrementAction")
            }
        }
    }
</script>

但是如果我的大仓库index.js也有一个increment呢,当我使用this.$store.commit("increment")的时候,其实两个地方的increment方法都会执行,只要你提交的方法名在他们俩的mutations中存在。

当我派发 this.$store.dispatch("incrementAction")的时候,他也是不知道我派发的是给哪一个模块里的actions,没有办法区分

如何避免这种问题?

home.js

const homeModule = {
    在这里为他加上命名空间就可以避免上述问题
    namespaced:true,
}

后续Home.vue调用homeModule:

home模块下的getters下的doubleHomeCounter方法
{{$store.getters["home/doubleHomeCounter"]}}

以前的写法:对比
{{$store.getters.doubleHomeCounter}}

getters的补充(home.js)

getters:{
    doubleHomeCounter(state,getters,rootState,rootGetters){
        return state.homeCounter*2
        
        //通过第二个参数拿到getters中的其他方法
        getters.otherGetter()
        
        //通过第三个参数获取根的state
        rootState.indexStateCounter
        
        //通过第四个参数获取根的getters
        
    },
    otherGetter(state){
        return 100
    }
},
actions:{
    incrementAction(context){
        context.commit
        context.dispatch
        context.state
        context.rootState
        context.getters
        context.rootGetters
        
        //null:表示不传payload
        //root:true表示传给root中mutations的increment
        context.commit("increment",null,{root:true})
        
        //再分发给根root里面actions中的increment
        context.dipatch("increment",null,{root:true})
    }

}

图片.png

图片.png

module的辅助函数

optional API写法

Home.vue

<template>
    <div></div>
</template>

<script>
    import{mapState,mapGetters,mapMutations,mapActions} from 'vuex' 
    export default{
    //vue2中直接将mapState和mapGetters包裹
        computed:{
        //写法一:
            ...mapState({
                homeCounter:state=>state.home.homeCounter
            }),
            ...mapGetters({
                doubleHomeCounter:"home/doubleHomeCounter"
            })
        }
            
        computed:{
        //写法二:
            ...mapState("home",["homeCounter"]),
            ...mapGetters("home",["doubleHomeCounter"])     
        },
             ---------------------------------------------------
        methods:{
        //写法一
            ...mapMutations({
                increment:"home/increment"
            }),
            ...mapActions({
                incrementAction:"home/incrementActions"
            })
        }
                   
        methods:{
        //写法二:
            ...mapMutations("home",["increment"]),
            ...mapActions("home",["incrementAction"])
        }
       ---------------------------------------------------          
    }
</script>

还有第三种写法,较为特殊,不用自带的mapState,更为常用

Home.vue

<template>
    <div></div>
</template>

<script>
   //引用一个createNamespaceHelpers import{createNamespaceHelpers} from 'vuex' 
   
   //放入对应的模块,告诉他我们用的全是home模块
   const {mapState,mapGetters,mapMutations,mapActions} =
   createNamespaceHelpers("home").mapState
   
    export default{
        computed:{
        //第三种方法
            ...mapState(["homeCounter"])
            ...mapGeters(["doubleHomeCounter"])
            
            ------------------和之前的对比
            ...mapGetters({
                doubleHomeCounter:"home/doubleHomeCounter"
            })
            
        },
        methods:{
            ...mapMutations(["increment"])
            ...mapActions(["incrementAction"])
            
            ------------------和之前的对比
            ...mapMutations("home",["increment"]),
        }
    }
</script>
Composition API写法

Home.vue

<template>
    <div></div>
</template>

<script>
    export default{
        setup(){
        //{homeCounter:function}
            const state = mapState(["homeCounter"])
            const getters = mapGetters(["doubleHomeCounter"])
            
            const mutations = mapMutations(["increment"])
            const actions = mapActions(["incrementAction"])
            return{
            //直接解构返回的是一个函数,是会报错的
                ...state
                ...getters
            
            //这两个本身使用的时候就是使用函数,所以解构使用无所谓
                ...mutations
                ...actions
            }
        }
    }
</script>

导入我们曾经写的hook函数,用来解构mapState和mapGetters import {useState,useGetters} from '../hooks/index' 但是我们写的hook函数使用只能调用根vuex中的State和Getters,我们还需要对hook函数进行相应的改进才能真正使用在模块中。

useState.js

//当我们使用.useState("home",['HomeCounter'])里面可能会写具体哪个模块里的哪个方法
如果我们还用mapState的,他是认不出来的,所以要额外导入createNamespacedHelpers。

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

//我们多添加一个参数,用来传模块的名字
export function useState(moduleName,mapper){
    let mapperFn = mapState

//对moduleName处理,可能也会不传,所以要先写个if
    if(typeof moduleName === 'string' && moduleName.length>0){
    //将具体模块名里的具体方法,赋值给mapperFn
        mapperFn = createNamespaceHelpers(moduleName).mapState
    } else{
    //我们可能直接传mapper,没有传Name
    但是接收参数会默认将第一个视为Name,也就是说Name接收到了mapper的值
    所以我这里再将mapper值重新赋一下
        mapper = moduleName
    }
    
    //我们先前用createNamespaceHelpers告诉他到底是哪来的方法
    //通过赋值给他,这样他就知道到底是哪个模块中的方法了
    return useMapper(mapper,mapperFn)
}

useGetters也是同样的道理

import {mapGetters,createNamespaceHelpers} from 'vuex'
import {useGetters} from './useGetters'

export function useGetters(moduleName,mapper){
    let mapperFn = mapGetters
    if(typeof moduleName === 'string'&&moduleName.length>0){
        mapperFn = createNamespaceHelpers(moduleName).mapGetters
    } else{
        mapper = moduleName
    }
    return useMapper(mapper,mapperFn)
}

图片.png

使用:

<script>
    export default{
        setup(){
        //{homeCounter:function}
            const state = useState(["home","homeCounter"])
            const getters = useGetters(["home","doubleHomeCounter"])
            
            const mutations = mapMutations(["increment"])
            const actions = mapActions(["incrementAction"])
            return{
                ...state
                ...getters
            
                ...mutations
                ...actions
            }
        }
    }
</script>

nexttick

图片.png

App_nexttick.vue

每次点击button,message都会增加内容

我希望每次点击完button,能够显示<h2 class="title">{{message}}</h2>标签的高度

<template>
    <div>
        <h2 class="title" ref="titleRef">{{message}}</h2>
        <button @click = "addMessageContent">
            添加内容
        </button>
    </div>
</template>

<script>
    import {ref} from 'vue'
    export default{
        setup(){
            const message = ref("123123")
            const titleRef = ref(null)
            
            const addMessageContent=()=>{
                message.value+="阿巴"
                //拿到这个DOM的高度
                console.log(titleRef.value.offsetHeight)
            }
            return{
                message,
                addMessageContent
            }
        }
    }
</script>
<style scoped>
    .title{
        width:120px;
    }
</style>

实际上返回的结果是:每一次点击按钮前 DOM的高度;我的要求是先更改掉界面的值,然后再获取最新的高度。并不是这样的结果。

我们最先想到的解决方法就是将获取DOM高度的操作,放在onUpdated生命周期函数中,每次DOM更新后,我就执行,不然不执行。

但是这样会带来局限性:如果我页面中才存在其他需要更新DOM的操作,我每次执行这些操作的时候,我存储在onUpdated生命周期函数里的获取高度的函数,他每次都要执行一次,这不纯纯的bug吗?

解决方法:使用nexttick

图片.png

他每次执行都是在DOM更新后

demo08.gif