vue2-vuex

75 阅读6分钟

1. 概念

明确 vuex 是什么,应用场景,优势

  1. 是什么:

    vuex 是一个 vue 的状态管理工具,状态就是数据。

    大白话:vuex 是一个插件,可以帮我们管理 vue 通用的数据 (多组件共享的数据) 例如:购物车数据 个人信息数据

    在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。

  2. 场景:

    某个状态 在 很多个组件 来使用 (个人信息)

    多个组件 共同维护 一份数据 (购物车)

  3. 优势:

    共同维护一份数据,数据集中化管理

    响应式变化

    操作简洁 (vuex提供了一些辅助函数)

  4. 原理图:

在这里插入图片描述

2. 搭建vuex环境(多组件数据共享环境)

基于脚手架创建项目,构建 vuex 多组件数据共享环境

创建一个空仓库

  1. 安装 vuex:npm install vuex@3 或 yarn add vuex@3

  2. 新建 vuex 模块文件,创建文件:src/store/index.js,专门存放 vuex

  3. 创建仓库

    Vue.use(Vuex)

    创建仓库 new Vuex.Store()

    //引入Vue核心库
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件
    Vue.use(Vuex)
    
    //准备actions对象——响应组件中用户的动作
    const actions = {}
    //准备mutations对象——修改state中的数据
    const mutations = {}
    //准备state对象——保存具体的数据
    const state = {}
    
    //创建并暴露store
    export default new Vuex.Store({
    	actions,
    	mutations,
    	state
    })
    
  4. main.js 导入挂载,在main.js中创建vm时传入store配置项

    //引入store
    import store from './store'
    
    //创建vm
    new Vue({
    	el:'#app',
    	render: h => h(App),
    	store
    })
    

检验:this.$store

3. 核心概念

明确如何给仓库 提供 数据,如何 使用 仓库的数据

3.1 state 状态

State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 中的 State 中存储。

在 state 对象中可以添加我们要共享的数据。

state 状态,即数据, 类似于vue组件中的data

区别:

  1. data 是组件自己的数据
  2. state 是所有组件共享的数据

3.1.1 提供数据

//创建仓库
const store = new Vuex.Store({
   state: {
       count: 101
   }
})

3.1.2 使用数据

3.1.2.1 store 直接访问
  • (1) this.$store

    模板中: {{ $store.state.xxx }}

    组件逻辑中: this.$store.state.xxx

  • (2) import 导入 store

    JS模块中: store.state.xxx

3.1.2.2 辅助函数 mapState

mapState是辅助函数,帮助我们把 state 中的数据 自动 映射到 组件的计算属性中

  1. 导入mapState:import { mapState } from 'vuex'

  2. 数组方式引入 state:mapState(['count']) 或对象方式引入 mapState({newCount: "count"}) (数组方式不常用,了解即可)

  3. 展开运算符映射

    computed: {
        //数组写法(常用)
        ...mapState(['count', 'sum'])
        
        //对象写法,若页面上有和state同名的data或者computed数据,可使用对象写法重命名一下,否则用数组写法,用着简洁
      ...mapState({countNewName:'count'}),
    }
    

3.2 mutations-处理同步

明确 vuex 同样遵循单向数据流,组件中不能直接修改仓库的数据(修改了vue默认也不会报错、监测的,因为监测需要成本),state数据的修改只能通过 mutations,可以通过 strict: true 开启严格模式进行调试,上线的时候不需要开启严格模式,否则影响性能

  1. 封装 mutation 函数(定义 mutations 对象,对象中存放修改 state 的方法)

    const store = new Vuex.Store({
        // 严格模式(有利于初学者,检测不规范代码=>上线需要关闭)
        strict: true,
        state: {
            count: 0
        },
    
        // 定义mutations
        mutations: {
            // 第一个参数是当前store的state属性
            //不带参数
            addCount (state) {
                state.count += 1
            },
            //带参数
            addCountNum (state, n) {
                state.count += n
            }
        }
    })
    
  2. 组件中使用commit提交调用 mutations

    // 不带参数
    this.$store.commit('addCount')
    
    // 带参数
    this.$store.commit('addCountNum', 参数)
    

注意:提交参数只能一个,如果有多个参数,包装成一个对象传递

3.2.1 辅助函数 - mapMutations

mapMutations 和 mapState 很像,它是把位于 mutations 中的方法提取了出来,映射到组件 methods 中(用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数)

mutations: {
    subCount (state, n) {
        state.count -= n
    },
}
import { mapMutations } from 'vuex'
methods: {
    //数组形式
    ...mapMutations(['subCount'])
    
    //对象形式
    ...mapMutations({subCountNewName:'subCount'}),
}

//相当于
methods: {
    subCount (n) {
        this.$store.commit('subCount', n)
    },
}
//调用,数组写法
this.subCount(10) 
//调用,对象写法
this.subCountNewName(10) 

3.3 actions-处理异步

需求:一秒钟之后,修改 state 的 count 成 666。

说明:mutations 必须是同步的 (便于监测数据变化,记录调试)

  1. 提供 action 方法

    actions: {
        setAsyncCount (context, num) {
            // 一秒后, 给一个数, 去修改 num
            setTimeout(() => {
                context.commit('changeCount', num)
            }, 1000)
        }
    }
    

    注意:actions 不能直接操作 state,操作 state,还是需要 commit mutations

  2. 页面中 dispatch 调用

    this.$store.dispatch('setAsyncCount', 200)
    

3.3.1 辅助函数 - mapActions

mapActions 是把位于 actions 中的方法提取了出来,映射到组件 methods 中(用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数)

actions: {
    changeCountAction (context, num) {
        setTimeout(() => {
            context.commit('changeCount', num)
        }, 1000)
    }
}
import { mapActions } from 'vuex'
methods: {
    //数组形式
    ...mapActions(['changeCountAction'])
    
    //对象形式
    ...mapActions({changeCountActionNewName:'changeCountAction'})
}

// 相当于
methods: {
    changeCountAction (n) {
        this.$store.dispatch('changeCountAction', n)
    },
}
// 调用,数组形式
this.changeCountAction(666)
// 调用,对象形式
this.changeCountActionNewName(666) 

3.4 getters-类似计算属性

说明:除了 state 之外,有时我们还需要从 state中 派生出一些状态,这些状态是依赖 state 的,此时会用到 getters

概念:当 state 中的数据需要经过加工后再使用时,可以使用 getters 加工。

  1. 定义 getters:在store.js中追加getters配置

    //用于将state中的数据进行加工
    getters: {
    	filterList(state){
            return state.list.filter(item => item > 5)
    	}
    }
    

    注意:

    getters函数的第一个参数是 state

    getters函数必须要有返回值

  2. 访问getters:组件中读取数据

    • 通过 store 访问 getters: $store.getters.filterList
    • 通过辅助函数 mapGetters 映射
    computed: {
    ...mapGetters(['filterList'])
    },
    
    //页面上调用
    {{ filterList }}
    

3.4.1 辅助函数 mapGetters

用于帮助我们映射getters中的数据为计算属性

import { mapGetters } from 'vuex'
 computed: {
     //数组写法
     ...mapGetters(['bigSum'])
     
     //对象写法
     ...mapGetters({bigSum:'bigSum'}),
 },

3.5 总结

备注:mapActions 与 mapMutations 使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则传的参数是事件对象(event)。

具体案例:

<template>
  <div>
    <h1>当前求和为:{{ sum }}</h1>
    <h3>当前求和放大10倍为:{{ bigSum }}</h3>
    <h3>年龄:{{ age }}</h3>
    <h3>姓名:{{name}}</h3>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
	<!-- 用了mapActions 和 mapMutations 的话要主动传参,如果不主动传参,传的是默认的event -->
    <button @click="increment(n)">+</button>
    <button @click="decrement(n)">-</button>
    <button @click="incrementOdd(n)">当前求和为奇数再加</button>
    <button @click="incrementWait(n)">等一等再加</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
export default {
  name: "Count",
  data() {
    return {
      n: 1, //用户选择的数字
    };
  },
  computed: {
	...mapState(['sum', 'age', 'name']),
	...mapGetters(['bigSum'])  
  },
  methods: {
    ...mapActions({incrementOdd: 'sumOdd', incrementWait: 'sumWait'}),
    ...mapMutations({increment: 'sum', decrement: 'reduce'})
  },
  mounted() {
    console.log("Count", this);
  },
};
</script>

<style lang="css">
button {
  margin-left: 5px;
}
</style>

  1. 初始化数据、配置actions、配置mutations,操作文件store.js

    //引入Vue核心库
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //引用Vuex
    Vue.use(Vuex)
    
    const actions = {
        //响应组件中加的动作
    	jia(context,value){
            // console.log('actions中的jia被调用了',miniStore,value)
            context.commit('JIA',value)
    	},
    }
    
    const mutations = {
        //执行加
    	JIA(state,value){
            // console.log('mutations中的JIA被调用了',state,value)
            state.sum += value
    	}
    }
    
    //初始化数据
    const state = {
       sum:0
    }
    
    //创建并暴露store
    export default new Vuex.Store({
    	actions,
    	mutations,
    	state,
    })
    
  2. 组件中读取vuex中的数据:$store.state.sum

  3. 组件中修改vuex中的数据:$store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)

    备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写dispatch,直接编写commit

具体案例:

index.js

//该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)

//准备actions——用于响应组件中的动作
const actions = {
    /* jia(context,value){
        console.log('actions中的jia被调用了')
        context.commit('JIA',value)
    },
    jian(context,value){
        console.log('actions中的jian被调用了')
        context.commit('JIAN',value)
    }, */
    jiaOdd(context,value){
        console.log('actions中的jiaOdd被调用了')
        if(context.state.sum % 2){
                context.commit('JIA',value)
        }
    },
    jiaWait(context,value){
        console.log('actions中的jiaWait被调用了')
        setTimeout(()=>{
                context.commit('JIA',value)
        },500)
    }
}
//准备mutations——用于操作数据(state)
const mutations = {
	JIA(state,value){
		console.log('mutations中的JIA被调用了')
		state.sum += value
	},
	JIAN(state,value){
		console.log('mutations中的JIAN被调用了')
		state.sum -= value
	}
}
//准备state——用于存储数据
const state = {
	sum:0 //当前的和
}

//创建并暴露store
export default new Vuex.Store({
	actions,
	mutations,
	state,
})

Count.vue

<template>
	<div>
		<h1>当前求和为:{{$store.state.sum}}</h1>
		<select v-model.number="n">
			<option value="1">1</option>
			<option value="2">2</option>
			<option value="3">3</option>
		</select>
		<button @click="increment">+</button>
		<button @click="decrement">-</button>
		<button @click="incrementOdd">当前求和为奇数再加</button>
		<button @click="incrementWait">等一等再加</button>
	</div>
</template>

<script>
	export default {
		name:'Count',
		data() {
			return {
				n:1, //用户选择的数字
			}
		},
		methods: {
			increment(){
                // commit 是操作 mutations
				this.$store.commit('JIA',this.n)
			},
			decrement(){
                // commit 是操作 mutations
				this.$store.commit('JIAN',this.n)
			},
			incrementOdd(){
                // dispatch 是操作 actions
				this.$store.dispatch('jiaOdd',this.n)
			},
			incrementWait(){
                // dispatch 是操作 actions
				this.$store.dispatch('jiaWait',this.n)
			},
		},
		mounted() {
			console.log('Count',this)
		},
	}
</script>

<style lang="css">
	button{
		margin-left: 5px;
	}
</style>

4. 模块 module (进阶语法)

由于 vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。(当项目变得越来越大的时候,Vuex会变得越来越难以维护)

目的:让代码更好维护,让多种数据分类更加明确。

image.png

模块拆分: user模块: store/modules/user.js

const state = {
    userInfo: {
        name: 'zs',
        age: 18
    }
}

const mutations = {}
const actions = {}
const getters = {}
export default {
    state,
    mutations,
    actions,
    getters
}
import user from './modules/user'
const store = new Vuex.Store({
    modules: {
        user
    }
})

4.1 state

尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的 state 中,属性名就是模块名

使用模块中的 state 数据:

  1. 直接通过模块名访问 $store.state.模块名.xxx

    开启不开启命名空间都是这样访问的

  2. 通过 mapState 映射

    默认根级别的映射 mapState([ 'xxx' ]):比如访问 user 为 mapState([ 'user' ]),页面上访问 {{user.userinfo}} 【开启不开启命名空间都是这样访问的】

    //子模块user,开启不开启命名空间都是这样访问的
    import { mapState } from 'vuex'
    computed: {
        ...mapState(['user'])
    },
    //页面上访问:{{user.userinfo}} 
    

    子模块的映射 mapState('模块名', ['xxx']) - 需要开启命名空间(在当前子模块内添加 namespaced: true)

    //user模块: store/modules/user.js
    export default {
        //user子模块开启命名空间
        namespaced: true,
        state,
        mutations,
        actions,
        getters
    }
    
    import { mapState } from 'vuex'
    computed: {
        //数组写法
        ...mapState('user',['userinfo']),
        //对象写法
        ...mapState('user',{userinfo: 'userinfo'}),
    },
    // 使用
    // 在页面上直接 {{userinfo}}
    

4.2 getters

注意:默认模块中的 getters 和 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

子模块未开启命名空间(和主模块访问方式一样)

  1. 通过 store 直接访问 $store.getters.xxx

  2. 通过 mapGetters 映射 mapGetters['xxx']

    //通过 store 访问 子模块的 getters: 
    $store.getters.filterList
    //通过辅助函数 mapGetters 映射
    computed: {
        ...mapGetters(['filterList'])
    },
    
    //页面上调用
    {{ filterList }}
    

子模块开启命名空间

  1. 直接通过模块名访问 $store.getters['模块名/xxx ']

  2. 通过 mapGetters 映射

    子模块的映射 mapGetters('模块名', ['xxx'])

    //user模块: store/modules/user.js
    export default {
        //子模块开启命名空间
        namespaced: true,
        state,
        mutations,
        actions,
        getters
    }
    
    //自己直接读取
    this.$store.getters['user/filterUserlist']
    
    //通过 mapGetters 映射
    import { mapGetters } from 'vuex'
    computed: {
        //数组写法
        ...mapGetters('user', ['filterUserlist']),
        //对象写法
        ...mapGetters('user', {filterUserlistNewName: 'filterUserlist'})
    }
    // 使用
    // 数组写法在页面上直接 {{filterUserlist}}
    // 对象写法在页面上直接 {{filterUserlistNewName}}
    

4.3 mutations

注意:默认模块中的 getters 和 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

不开启命名空间的访问方式和读取方式和主模块一样,这里只展示开启命名空间方式的例子

调用子模块中 mutation:

  1. 直接通过 store 调用 $store.commit('模块名/xxx ', 额外参数)

  2. 通过 mapMutations 映射

    子模块的映射 mapMutations('模块名', ['xxx']) - 需要开启命名空间

//方式一:自己直接commit
this.$store.commit('personAbout/ADD_PERSON', person)
//方式二:借助mapMutations:
import { mapMutations } from 'vuex'
methods: {
    ...mapMutations('personAbout', ['ADD_PERSON']),
}

4.4 actions

注意:默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。

不开启命名空间的访问方式和读取方式和主模块一样,这里只展示开启命名空间方式的例子

调用子模块中 action :

  1. 直接通过 store 调用 $store.dispatch('模块名/xxx ', 额外参数)

  2. 通过 mapActions 映射

    子模块的映射 mapActions('模块名', ['xxx']) - 需要开启命名空间

//方式一:自己直接dispatch
this.$store.dispatch('personAbout/addPersonWang', person)
//方式二:借助mapActions:
...mapActions('personAbout', ['addPersonWang'])

4.5 模块化+命名空间总结

目的:让代码更好维护,让多种数据分类更加明确。

image.png

具体案例: count.js

//求和相关的配置
export default {
    namespaced:true,
    actions:{
        jiaOdd(context,value){
            console.log('actions中的jiaOdd被调用了')
            if(context.state.sum % 2){
                context.commit('JIA',value)
            }
        },
        jiaWait(context,value){
            console.log('actions中的jiaWait被调用了')
            setTimeout(()=>{
                    context.commit('JIA',value)
            },500)
        }
    },
    mutations:{
        JIA(state,value){
            console.log('mutations中的JIA被调用了')
            state.sum += value
        },
        JIAN(state,value){
            console.log('mutations中的JIAN被调用了')
            state.sum -= value
        },
    },
    state:{
        sum:0, //当前的和
        school:'尚硅谷',
        subject:'前端',
    },
    getters:{
        bigSum(state){
                return state.sum*10
        }
    },
}

person.js

//人员管理相关的配置
import axios from 'axios'
import { nanoid } from 'nanoid'
export default {
    namespaced:true,
    actions:{
        addPersonWang(context,value){
            if(value.name.indexOf('王') === 0){
                context.commit('ADD_PERSON',value)
            }else{
                alert('添加的人必须姓王!')
            }
        },
        addPersonServer(context){
            axios.get('https://api.uixsj.cn/hitokoto/get?type=social').then(
                response => {
                    context.commit('ADD_PERSON',{id:nanoid(),name:response.data})
                },
                error => {
                    alert(error.message)
                }
            )
        }
    },
    mutations:{
        ADD_PERSON(state,value){
            console.log('mutations中的ADD_PERSON被调用了')
            state.personList.unshift(value)
        }
},
    state:{
        personList:[
            {id:'001',name:'张三'}
        ]
    },
    getters:{
        firstPersonName(state){
            return state.personList[0].name
        }
    },
}

index.js

//该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
import countOptions from './count'
import personOptions from './person'
//应用Vuex插件
Vue.use(Vuex)

//创建并暴露store
export default new Vuex.Store({
    modules:{
            countAbout: countOptions,
            personAbout: personOptions
    }
})

count.vue

<template>
    <div>
        <h1>当前求和为:{{sum}}</h1>
        <h3>当前求和放大10倍为:{{bigSum}}</h3>
        <h3>我在{{school}},学习{{subject}}</h3>
        <h3 style="color:red">Person组件的总人数是:{{personList.length}}</h3>
        <select v-model.number="n">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
        </select>
        <button @click="increment(n)">+</button>
        <button @click="decrement(n)">-</button>
        <button @click="incrementOdd(n)">当前求和为奇数再加</button>
        <button @click="incrementWait(n)">等一等再加</button>
    </div>
</template>

<script>
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
export default {
    name:'Count',
    data() {
        return {
                n:1, //用户选择的数字
        }
    },
    computed:{
        //借助mapState生成计算属性,从state中读取数据。(数组写法)
        ...mapState('countAbout',['sum','school','subject']),
        ...mapState('personAbout',['personList']),
        //借助mapGetters生成计算属性,从getters中读取数据。(数组写法)
        ...mapGetters('countAbout',['bigSum'])
    },
    methods: {
        //借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(对象写法)
        ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
        //借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(对象写法)
        ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
    },
    mounted() {
        console.log(this.$store)
    },
}
</script>

<style lang="css">
    button{
        margin-left: 5px;
    }
</style>

person.vue

<template>
    <div>
        <h1>人员列表</h1>
        <h3 style="color:red">Count组件求和为:{{sum}}</h3>
        <h3>列表中第一个人的名字是:{{firstPersonName}}</h3>
        <input type="text" placeholder="请输入名字" v-model="name">
        <button @click="add">添加</button>
        <button @click="addWang">添加一个姓王的人</button>
        <button @click="addPersonServer">添加一个人,名字随机</button>
        <ul>
            <li v-for="p in personList" :key="p.id">{{p.name}}</li>
        </ul>
    </div>
</template>

<script>
    import {nanoid} from 'nanoid'
    export default {
        name:'Person',
        data() {
            return {
                    name:''
            }
        },
        computed:{
            personList(){
                return this.$store.state.personAbout.personList
        },
            sum(){
                return this.$store.state.countAbout.sum
            },
            firstPersonName(){
                return this.$store.getters['personAbout/firstPersonName']
            }
        },
        methods: {
            add(){
                const personObj = {id:nanoid(),name:this.name}
                this.$store.commit('personAbout/ADD_PERSON',personObj)
                this.name = ''
            },
            addWang(){
                const personObj = {id:nanoid(),name:this.name}
                this.$store.dispatch('personAbout/addPersonWang',personObj)
                this.name = ''
            },
            addPersonServer(){
                this.$store.dispatch('personAbout/addPersonServer')
            }
        },
    }
</script>