前面介绍过几种可以实现组件间通信的方式props、ref、自定义事件绑定、全局事件总线、插槽…,这些要么就是实现组件间通信只能在特定条件下使用,要么就是实现起来太复杂,今天介绍到的vuex是Vue中一款强大的插件,使用vuex后可以实现任意组件间通信,并且支持模块化管理。接下来将会围绕一个计数案例进行展开,逐步引入vuex如何使用。
核心代码:
<template> <div> <h2>目前的数值结果是:{{n}}</h2> <div class="container1"> <select v-model.number="step"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="addNum">增加</button> <button @click="subNum">减少</button> <button @click="oddNum">偶数再加</button> <button @click="longTimeAdd">延迟增加</button> </div> </div> </template> <script> export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { n:0, step:1 } }, methods:{ addNum(){ this.n+=this.step }, subNum(){ this.n-=this.step }, oddNum(){ if (this.n%2==0){ this.n+=this.step } }, longTimeAdd(){ setTimeout(() => { this.n+=this.step }, 1000); } } } </script> <style> .container1>*{ margin-left: 10px; } </style>
简单点说:是一个数据仓库,用于数据存储,数据共享,可以进行数据存放与获取,并全程监视数据的动向
使用官方语言入下:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
这个状态自管理应用包含以下几个部分:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
多个视图依赖于同一状态。
来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
这就是 Vuex 背后的基本思想,借鉴了 Flux、Redux 和 The Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
vuex适用于大型单页应用的开发,如果是小型的不推荐使用。
其中vuex3适配vue2,vuex4适配vue3。
vue工作原理:
vuex中主要包含三个部分
vuex中的两种数据操作
vuex在项目中使用
大致有两种(实质上一样):
安装vuex
npm i vuex
继续从以下三个模块与两个方法讲起:
三个模块:
两个方法:
如果调用vuex的dispatch方法会将数据提交到Actions中的函数,此时数据并不是最终的结果,在Actions中可以对数据进行进一步加工,然后调用commit方法提交,在Actions与Mutations中定义的函数可以获取到上下文找到state对象中存储的数据。
接下来结合代码看一下vuex如何使用(这一段代码暴露出去一个可以使用的vuex对象):
import Vue from "vue" import Vuex from "vuex" Vue.use(Vuex) const actions={ odd_Num(comtext,value){ // 可以通过上下文comtext找到commit方法,dispatch方法 // 可以找到state对象,并找到其中的属性 if (comtext.state.n%2===0){ comtext.commit("add_Num",value) } }, long_Time_Add(comtext,value){ setTimeout(() => { comtext.commit("add_Num",value) }, 1000); } } const mutations={ add_Num(state,value){ // 这里的state被称为上下文,这里可以拿到state中的数据 // console.log(state,value) state.n+=value }, sub_Num(state,value){ // console.log(state,value) state.n-=value }, } //数据源,源数据 const state={ n:0 } //创建一个vuex对象 export default new Vuex.Store({ actions, mutations, state })
以下代码是使用vuex:
main.js
import App from "./App" //这个便是刚才封装的vuex对象 import store from "./store" import Vue from "vue" new Vue({ el:"#App", store, render:h=>h(App) })
功能性组件:
export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { step:1 } }, computed:{ n(){ return this.$store.state.n } } , methods:{ addNum(){ this.$store.commit("add_Num",this.step) }, subNum(){ this.$store.commit("sub_Num",this.step) }, oddNum(){ this.$store.dispatch("odd_Num",this.step) }, longTimeAdd(){ this.$store.dispatch("long_Time_Add",this.step) } } }
除此之外vuex还有一个重要的模块供我们使用getters,这里面的方法通常用于获取定制后的state中的属性
比如下面一个实例,就是将state中的n扩大10倍后返回了出去。
const getters={ numBig(state){ return state.n*10 } }
如果vuex中的函数较多时,一个一个引入会相当麻烦,官方给出了四个映射函数,可以根据自己的需求将想要的函数一下引进来,分别是:mapState,mapGetters,mapActions,mapMutations。
以下两种写法,在使用时效果是一样的
不使用映射函数:
<script> export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { step:1 } }, computed:{ n(){ return this.$store.state.n }, bigNum(){ // 以下两种写法均可以 // return this.$store.getters["numBig"] return this.$store.getters.numBig } } , methods:{ addNum(){ this.$store.commit("add_Num",this.step) }, subNum(){ this.$store.commit("sub_Num",this.step) }, oddNum(){ this.$store.dispatch("odd_Num",this.step) }, longTimeAdd(){ this.$store.dispatch("long_Time_Add",this.step) } } } </script>
使用映射函数:
<script> import {mapState,mapGetters,mapActions,mapMutations} from "vuex" export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { step:1 } }, computed:{ ...mapState(["n"]), ...mapGetters({bigNum:"numBig"}) } , methods:{ // 传入对象是常规写法,当对象中的键值相等时,可以用传入列表的方式进行传参 // 例如上面的mapState(["n"]) ...mapMutations({addNum:"add_Num",subNum:"sub_Num"}), ...mapActions({oddNum:"odd_Num",longTimeAdd:"long_Time_Add"}) } } </script>
经过将多个组件使用到的数据提取到vuex,可以实现多组件间的数据共享,也就是说在a组件修改共享数据之后,在b组件可以渲染出数据改变后的结果,同样b组件中修改a组件也可以渲染。
以下案例一个计数组件负责计数,一个person组件,负责将人员列表渲染出来,person组件支持增加用户,我们要做的就是person组件中进行人员添加后,计数组件感应到人数变动,重新渲染列表,将总人数进行改变。
vuex插件
import Vue from "vue" import Vuex from "vuex" import {nanoid} from "nanoid" Vue.use(Vuex) const actions={ odd_Num(comtext,value){ // 可以通过上下文comtext找到commit方法,dispatch方法 // 可以找到state对象,并找到其中的属性 if (comtext.state.n%2===0){ comtext.commit("add_Num",value) } }, long_Time_Add(comtext,value){ setTimeout(() => { comtext.commit("add_Num",value) }, 1000); }, add_Person(context,value){ var someone={id:nanoid(),name:value} context.commit("add_Person",someone) } } const mutations={ add_Num(state,value){ // 这里的state被称为上下文,这里可以拿到state中的数据 // console.log(state,value) state.n+=value }, sub_Num(state,value){ // console.log(state,value) state.n-=value }, add_Person(state,value){ state.personList.unshift(value) } } const state={ n:0, personList:[ { id:"0001", name:"张三" } ] } const getters={ numBig(state){ return state.n*10 }, personNum(state){ return state.personList.length // console.log("index"+state.personList.length) } } export default new Vuex.Store({ actions, mutations, state, getters })
计数组件
<template> <div> <h2>目前的数值结果是:{{n}}</h2> <h2>放大后的数值结果是:{{bigNum}}</h2> <div class="container1"> <select v-model.number="step"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="addNum(step)">增加</button> <button @click="subNum(step)">减少</button> <button @click="oddNum(step)">偶数再加</button> <button @click="longTimeAdd(step)">延迟增加</button> </div> <h2>当前的总人数为{{personNum}}</h2> </div> </template> <script> import {mapState,mapGetters,mapActions,mapMutations} from "vuex" export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { step:1 } }, computed:{ ...mapState(["n"]), ...mapGetters({bigNum:"numBig",personNum:"personNum"}) } , methods:{ // 传入对象是常规写法,当对象中的键值相等时,可以用传入列表的方式进行传参 // 例如上面的mapState(["n"]) ...mapMutations({addNum:"add_Num",subNum:"sub_Num"}), ...mapActions({oddNum:"odd_Num",longTimeAdd:"long_Time_Add"}) } } </script> <style> .container1>*{ margin-left: 10px; } </style>
person组件
<template> <div> <h2>目前的计算结果为{{n}}</h2> //监听的键盘事件,当输入enter时添加用户 <input v-model="tempname" @keydown.enter="add_Person(tempname)"> <ul> <li v-for="p in personList" :key="p.id"> 学生id:{{p.id}} 学生姓名:{{p.name}} </li> </ul> </div> </template> <script> import {mapState,mapActions} from "vuex" export default { // eslint-disable-next-line vue/multi-word-component-names name:"Persons", data(){ return { tempname:"" } }, computed:{ ...mapState(["n","personList"]) }, methods:{ ...mapActions(["add_Person"]) } } </script> <style> </style>
如果不按照数据功能对数据进行模块划分,那么vuex将会显得非常杂乱,我们希望vuex可以根据数据的不同作用将其进行模块化,便于我们管理,对于接下来的代码大家需要熟悉的是如何使用划分后的函数、函数映射。
划分后的代码大概长这个样子,其中store中的index会将count.js与person.js引入并使用。vue代码中使用vuex依旧是引入index.js,值得注意的是进行模块划分后的vuex,在使用时跟以往有了不同。详细看代码
index.js
import Vue from "vue" import Vuex from "vuex" import counts from "./count" import person from "./person" Vue.use(Vuex) export default new Vuex.Store({ modules:{ counts, person } })
count.js
// import axios from "axios" export default{ namespaced:true, actions:{ /* ... */ }, mutations:{ /* ... */ }, state:{ n:0, }, getters:{ numBig(state){ return state.n*10 } } }
person.js
import {nanoid} from "nanoid" export default{ namespaced:true, actions:{ /* ... */ }, mutations:{ /* ... */ }, state:{ personList:[ { id:"0001", name:"张三" } ] }, getters:{ personNum(state){ return state.personList.length // console.log("index"+state.personList.length) } } }
count.vue
<template> <div> <h2>目前的数值结果是:{{n}}</h2> <h2>放大后的数值结果是:{{bigNum}}</h2> <div class="container1"> <select v-model.number="step"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="addNum(step)">增加</button> <button @click="subNum(step)">减少</button> <button @click="oddNum(step)">偶数再加</button> <button @click="longTimeAdd(step)">延迟增加</button> </div> <h2>当前的总人数为{{personNum}}</h2> </div> </template> <script> import {mapState,mapGetters,mapActions,mapMutations} from "vuex" export default { // eslint-disable-next-line vue/multi-word-component-names name:"Counts", data(){ return { step:1 } }, computed:{ ...mapState("counts",["n"]), ...mapGetters("counts",{bigNum:"numBig"}), ...mapGetters("person",{personNum:"personNum"}) } , methods:{ // 传入对象是常规写法,当对象中的键值相等时,可以用传入列表的方式进行传参 // 例如上面的mapState(["n"]) ...mapMutations("counts",{addNum:"add_Num",subNum:"sub_Num"}), ...mapActions("counts",{oddNum:"odd_Num",longTimeAdd:"long_Time_Add"}) } } </script> <style> .container1>*{ margin-left: 10px; } </style>
persons.vue
<template> <div> <h2>目前的计算结果为{{n}}</h2> <input v-model="tempname" @keydown.enter="addPerson(tempname)"> <button v-if="tag" @click="tag=!tag">目前可增加任意姓氏</button> <button v-if="!tag" @click="tag=!tag">目前只可以添加王氏</button> <ul> <li v-for="p in personList" :key="p.id"> 学生id:{{p.id}} 学生姓名:{{p.name}} </li> </ul> </div> </template> <script> import {mapState,mapActions} from "vuex" export default { // eslint-disable-next-line vue/multi-word-component-names name:"Persons", data(){ return { tag:true, tempname:"" } }, computed:{ ...mapState("counts",["n"]), ...mapState("person",["personList"]) }, methods:{ ...mapActions("person",["add_Person"]), addPerson(){ if (this.tempname===""){ return "" } if(this.tag){ // 映射出来的函数 this.add_Person(this.tempname) }else{ //[Vue warn]: Error in v-on handler: "TypeError: this.addPersonWang is not a function" // 如果没有使用映射的话,不能这样使用 // this.addPersonWang(this.tempname) // 正确的使用方式(使用模块命名空间进行限定) this.$store.dispatch("person/addPersonWang",this.tempname) } this.tempname="" }, } } </script> <style> </style>
除此之外还可以通过下面方法获取到vuex中的state中的属性。
this.$store.state.模块名.属性
this.$store.state.loginModel.userInfo