vue进阶:vuex(数据池)
- 非父子组件传值
- vuex
一、非父子组件传值
基于父子组件通信与传值实现非父子组件传值的示例关键代码:
1 <template> 2 <div> 3 <!-- 学员展示 --> 4 <add-student @add="add"></add-student> <!--监听子组件自定义事件add,触发自身add事件--> 5 <student-list :student-list="studentList"></student-list> <!---基于v-bind建立数据通道--> 6 </div> 7 8 </template> 9 10 <script> 11 import AddStudent from '@/components/student/AddStudent' 12 import StudentList from '@/components/student/StudentList' 13 14 export default { 15 components:{ 16 AddStudent, 17 StudentList 18 }, 19 data(){ 20 return { 21 studentList:[] 22 } 23 }, 24 methods:{ 25 add(name){ //调用由addStudent触发的add事件,实现向父级data中添加数据 26 this.studentList.push(name); 27 } 28 } 29 } 30 </script>
1 <template> 2 <div> 3 添加学生: 4 <input type="text" v-model="name"/> 5 <button @click="add">确认添加</button> 6 </div> 7 </template> 8 9 <script> 10 11 export default { 12 data(){ 13 return { 14 name:'' 15 } 16 }, 17 methods:{ 18 add(){ 19 this.$emit('add',this.name) //触发父级监听事件add 20 } 21 } 22 }
1 <template> 2 <div> 3 学生列表: 4 <ul> 5 <li v-for="(item,index) in studentList" 6 :key="index+item"> 7 {{item}} 8 </li> 9 </ul> 10 </div> 11 12 </template> 13 14 <script> 15 export default { 16 props:['student-list'] 17 } 18 </script>
通过这个简单的示例看示可以通过上面的方法解决非父子组件传值问题,那么问题来了,实际的业务需求中如果出现了很多个组件依赖一个组件传值呢?而且还可能出现组件嵌套层级更复杂的情况:(示图中的依赖A是表示依赖A传值,不是依赖组件A)
当遇到这种复杂的依赖时,我们会想到它们都是基于父级$emit来监听组件A,然后在通过父级的数据变化来相渲染,那是不是就可以通过一个vue实例在组件A中监听组件A,并且每个依赖A的组件都可以访问得到该vue实例,然后再在每个依赖A的组件中触发监听事件:
事件监听vueObje.$emit:
在vue原型链上绑定实例vue.prototype.vueObje;
在每个依赖A的组件中触发监听事件:this.vueObje.$on('监听事件', 数据 =>{数据渲染})
所以上面的示例可以做以下修改:
Vue.prototype.bus = new Vue(); // 在main.js入口文件中给vue的原型链上添加vue实例bus
Student.vue组件模板修改:
<template> <div> <add-student></add-student> <student-list></student-list> </div> </template>
AddStudent.vue组件中触发bus的监听事件:
1 methods:{ 2 add(){ 3 this.bus.$emit('add',this.name) //触发父级监听事件add 4 } 5 }
StudentList.vue组件中在实例创建完成以后使用钩子函数created()添加bus的监听事件:(注意:需要将props去掉)
1 created(){ 2 this.bus.$on('add',name => { 3 this.studentList.push(name); 4 }) 5 }
基于单个组件向多个非父子组件传值可以使用vue实例的事件监听机制来实现,但是如果业务需求是多个组件拥有修改数据的权限,并且同时影响多个组件的数据渲染,这种复杂的数据关系那又怎么办?
这时候就可以使用vuex来解决这种相对比较复杂的数据关系,并且vuex有更完善的数据管理模式,进入第二节详细解析vuex的应用:
二、vuex
官方手册:https://vuex.vuejs.org/zh/
安装:vue add vuex
安装完成以后在src文件夹下新增了一个js文件:store.js
1 import Vue from 'vue' //引入vue 2 import Vuex from 'vuex' //引入vuex 3 4 Vue.use(Vuex) //让vue使用vuex 5 6 export default new Vuex.Store({ //通过vuex.Store创建vuex实例 7 state: { 8 9 }, 10 mutations: { 11 12 }, 13 actions: { 14 15 } 16 })
然后还要再入口文件中引入这个vuex实例:
import store from './store' //main.js中引入vuex实例
在store.js的state中添加数据字段,然后在组件中使用:
1 state: { 2 //公共数据池 3 name:'他乡踏雪' 4 },
在组件a中使用公共数据池的数据:
1 <template> 2 <div> 3 {{storeName}} 4 </div> 5 </template> 6 //js数据 7 data(){ 8 return { 9 storeName:this.$store.state.name 10 } 11 },
在组件b中使用公共数据池的数据:
1 <template> 2 <div> 3 {{$store.state.name}} 4 </div> 5 </template>
然后在组件c中通过点击事件修改公共数据池的数据:
1 <button @click="ceshi">确认添加</button> 2 ... 3 methods:{ 4 ceshi(){ 5 this.$store.state.name += 1; 6 } 7 }
在没有触发点击事件之前,在页面上能看正确的渲染结果“他乡踏雪”,但是当触发ceshi点击事件后,只能看到组件b中的数据更新了“他乡踏雪1”,这个问题并不在vuex,而是组件a中的逻辑出现了错误,因为在data中赋值的是字符串,storeName的栈内存是“他乡踏雪”,而并非$store.state.name的引用依赖,所以当$store.state.name被修改后并不能实现a组件的storeName的修改,这是模板语法中计算属性的相关内容:https://www.cnblogs.com/ZheOneAndOnly/p/11003014.html
所以,在组件a中应该使用计算属性来实现数据绑定:
1 computed:{ 2 storeName(){ 3 return this.$store.state.name 4 } 5 }
按照上面这种直接使用this.$store.state.name的方式获取数据没有什么错误,但是如果有非常多的数据需要获取还是这么写吗?看下面的操作:
1 //store.js 2 state: { 3 //公共数据池 4 name:'他乡踏雪', 5 age:18 6 } 7 //组件a 8 <script> 9 import {mapState} from 'vuex' 10 11 xport default { 12 data(){ 13 name:'' 14 }, 15 computed:mapState(['name','age']) 16 }
这时后在页面上你能正确的看到{{age}}的渲染数据,但是发现{{name}}没有正确渲染,原因又处在模板语法中优先使用了data中的数据,与计算属性computed中获取到的name冲突了,这种情况我们只能考虑给计算属性中的数据做别名处理,毕竟修改data中的数据名称在实际业务中不大可能,不用但是vuex还提供了键值对的方式获取数据池中的数据:
1 computed:mapState({ 2 storeName:state => state.name, 3 storeAge: state => state.age 4 })
显然上面的写法还有一个问题,使用将mapState直接赋值给了计算属性字段computed,那如果需要使用计算属性定义组件自身的数据呢?mapState整体返回的是一个对象,但对象内部的每一项都是一个函数,与computed的结构一样,所以就可以使用ES6的写法展开这个对象:(获取数据池中的最后的正确写法)
1 computed:{ 2 ...mapState({ 3 storeName:state => state.name, 4 storeAge: state => state.age 5 }), 6 a (){ 7 return 'aaaa' 8 } 9 }
如果在一个应用中出现了数据的派生模式,而且还有多个组件依赖同一个派生模式,这时候我们一定会想到计算属性,在vuex中也有类似computed的属性getters:
1 state: { 2 //公共数据池 3 studentList:[] 4 }, 5 getters:{ 6 // 相当于计算属性 7 newStudent(state){ 8 return state.studentList.map((item,index) => { 9 if(index == 0){ 10 return '**' + item 11 }else if(index == 1){ 12 return item + '**' 13 }else{ 14 return '++' + item + '++' 15 } 16 }) 17 } 18 }
然后在组件中获取这个getters的派生数据:
1 computed:{ 2 newStudent(){ 3 return this.$store.getters.newStudent //获取store中的getters的派生数据 4 } 5 }
当然同样也可以使用map方法来获取数据的对象模式:
1 import { mapState, mapGetters } from 'vuex'; //通过map方式获取数据 2 // 3 computed:{ 4 ...mapGetters(['newStudent']) 5 }
getters的数据别名就不需要state参数来获取了,其实state中的数据也可以不用,但是为了区分state和getters中的数据,当然还有防止命名冲突问题:
...mapGetters({student:'newStudent'})
最后,getters在构建派生数据时,还可以传入第二个参数,这个数据就是自身getters,然后通过自生可以在构建派生数据时引用自身的数据:
1 newA(state,getters){ 2 return state.studentList.map(item => { 3 return item + getters.newB 4 }) 5 }, 6 newB(){ 7 return 'B' 8 }
通过前面的state和getters解决了数据获取的统一方式,但是数据写入和修改呢?如果有多个组件有同样的数据写入和修改操作,还是要到每个组件中去重复的写吗?这明显不符合程序的设计思想,所以vuex提供了Mutation和Actiony来解决数据写入操作:
而且在vuex的store实例中有一个strict字段,这个字段如果被赋值为true时,表示该store采用严格模式,不能在外部写入和修改数据,不然会报错:
也就说合理的修改数据方式应该定义在Mutation中:
1 export default new Vuex.Store({ 2 strict:true, 3 state: { 4 //公共数据池 5 studentList:[] 6 }, 7 getters:{}, 8 mutations: { 9 changeStudent(state,name){ 10 state.studentList.push(name); 11 } 12 }, 13 actions: {} 14 })
然后在组件重调用这个方法:【通过$store.commit()调用mutations中的方法】
1 <button @click="add">确认添加</button> 2 ... 3 methods:{ 4 add(){ 5 this.$store.commit('changeStudent',this.name) 6 } 7 }
需要注意的是,commit只能接收两个参数,第一个参数是要调用mutations中的方法名称,第个参数是执行方法需要的参数,所以如果有多个参数就需要使用对象的键值对方式来传值(上面的代码可以修改为):
1 //mutations 2 changeStudent(state,{name}){ //使用键值对的方式接收 3 state.studentList.push(name); 4 } 5 //组件中的方法传值也同样采用键值对的方式 6 add(){ 7 this.$store.commit('changeStudent',{name:this.name}) 8 }
接着新的问题又出现了,一般情况下的写入和修改可以符合严格模式的要求,如果有异步的指令则使用mutations来操作数据的话由于this指向了全局同样会导致不符合严格模式的编程规范:
1 mutations: { 2 changeStudent(state,{name}){ 3 setTimeout(() => { 4 state.studentList.push(name); 5 },1000) 6 } 7 }
这样的写法同样会导致前面一样的问题出现,而且还会导致一些其他辅助性工作也不好做,比如使用vue.js devtools没有办法准确追踪执行栈:
解决store中的异步数据写入操作就是使用Actiony来解决:
1 mutations: { 2 changeStudent(state,{name}){ 3 state.studentList.push(name); 4 } 5 }, 6 actions: { 7 changeStudent(ctx,{name}){ //ctx表示数据操作的执行上下文 8 setTimeout(() => { 9 ctx.commit('changeStudent',{name}) //在异步程序中通过ctx.commit调用mutations中的数据操作方法 10 },1000) 11 } 12 } 13 //在组件方法中调用actions中的异步数据操作方法: 14 methods:{ 15 add(){ 16 this.$store.dispatch('changeStudent',{name:this.name}) //调用actions中的方法要使用dispatch方法来调用,语法与commit一致 17 } 18 },
到了这里解决了实际一些问题以后我么又会发现调用同各国this.$store.dispatch()调用actions中的方法有必要在组件实例中的methods中写一个方法来调用吗?可不可以和mapState、mapGetters一样,使用map的方法直接引入store中的方法,这个当然也是可行的,语法也基本一致:
1 <button @click="changeStudent">确认添加</button> 2 ... 3 import {mapState, mapActions} from 'vuex' 4 ... 5 methods:{ 6 ...mapActions(['changeStudent']) //同样使用键值对的方式实现别名 7 }
由于store使用单一状态树,所有数据状态集成到一个对象上,当应用变得非常复杂时,store对象就会变得非常复杂。为了解决这个问题,在store上还提供了一个modules字段来提供作为数据模块化的处理,每个模块都由自己的state、mutation、action、getter、modules(子模块依然可以嵌套模块)。
store子模块与store的区别就是要在子模块中注明为命名空间。
1 //子模块 2 const moduleA = { 3 namespaced:true, 4 state:{}, 5 mutations:{}, 6 getter:{}, 7 actions:{}, 8 modules:{} 9 } 10 const moduleB ={...} 11 12 //store主模块 13 export default new Vuex.Store({ 14 state:{}, 15 mutations:{}, 16 getter:{}, 17 actions:{}, 18 modules:{ 19 moduleA, 20 abc:moduleB//注意这里是‘abc’表示模块的命名空间 21 } 22 }
在Vue模块中使用store子模块的数据:
1 //直接使用store中的子模块数据 2 this.$store.state[--子模块的空间命名--][--数据项--] 3 4 //通过mapState使用子模块数据 5 mapState('模块的空间命名',[--数据项--]) 6 7 computed:{ 8 ...mapState('模块的空间命名',{xxx: state => state[--数据项--]}) 9 } 10 11 //使用store中的方法 12 this.$store.commit('模块的空间命名/方法名',参数)
store子模块当然可以使用独立的js模块文件
1 //storeUser.js 2 export default { 3 state:{} 4 ... 5 } 6 7 //store 8 export default new Vuex.Store({ 9 ... 10 modules: { 11 user, 12 course 13 } 14 })