目录
vue项目中大量采用组件化开发,组件之间的通信传递数据是必不可少的一个环节,并且随着需求的不同,需要进行不同关系的组件之间的值传递,从而出现各种组件通信的方式。
组件和组件之间的关系常见有父子组件,兄弟组件,两类关系的组件通信适用的方法是不同的,方法使用的不当,极有可能事倍功半;最常见的父子组件通信方式有props传参,常用的兄弟组件通信方式有自定义事件,全局事件总线;较大型的项目,一般采用的是vuex统一管理数据;此外,在一些组件库中,也经常会使用到其它几种通信方式,比如element-ui中,当你需要在一个el-table-column中插入一张图片时,就需要使用作用域插槽,将要展现的页面结构传递给子组件;再比如,在el-dialog弹出框组件中,会使用sync修饰符,获取控制组件显示或隐藏的标志符等等。以下10种组件通信方式总结如下。
一.props传参
适用于父子组件之间传递参数
(1).父组件 ==> 子组件 通信
(2).子组件 ==> 父组件 通信(要求父先给子一个函数)
1.父组件 ==> 子组件
传递数据:在子组件标签上写明传递的参数名=“参数”,若参数是js表达式,需要使用v-bind
<Demo :name="xxx"/>
接收数据:在子组件内部
1. 第一种方式(只接收)
props:['name']
2. 第二种方式(限制类型)
props:{name:String}
3. 第三种方式(限制类型、限制必要性、指定默认值):
props:{
name:{
type:String, //类型
required:true, //必要性
default:'老王' //默认值
}
}
2.子组件 ==> 父组件
若使用props在父组件中获取子组件的数据:这里在父组件中将addTodo()方法传递给子组件
<template>
<div id="root">
<MyHeader :addTodo="addTodo"/>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
export default {
name:'App',
components:{MyHeader},
data() {
return {
todos:[
{id:'001',title:'xx',done:true},
]
}
},
methods: {
//添加一个todo
addTodo(todoObj){
this.todos.unshift(todoObj)
},
}
}
</script>
在子组件内部获取input输入框的输入后,将输入信息封装成一个对象todoObj,调用父组件传来的addTodo()方法 ,以实参的形式将对象todoObj传给父组件,实现子 --> 父传参
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'MyHeader',
//接收从App传递过来的addTodo
props:['addTodo'],
data() {
return {
//收集用户输入的title
title:''
}
},
methods: {
add(){
//校验数据
if(!this.title.trim()) return alert('输入不能为空')
//将用户的输入包装成一个todo对象
const todoObj = {id:nanoid(),title:this.title,done:false}
//通知App组件去添加一个todo对象
this.addTodo(todoObj)
//清空输入
this.title = ''
}
},
}
</script>
props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。
二.组件的自定义事件
1.是一种组件间的通信方式,适用于:子组件 => 父组件
2.使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件。
3. 使用
绑定事件:在父组件中使用@自定义事件名 ,不用写形参,事件回调的形参会自动接收最组件传递的实参内容
<template>
<Demo @xxx="test">
或
<Demo v-on:xxx="test">
</template>
<script>
export default {
methods:{
test(事件参数1,...事件参数2-n){
}
}
}
</script>
或使用ref获取子组件实例对象,调用组件实例.$on('事件名',回调函数)用于监听子组件中定义的xxx事件;
<template>
<Demo ref="demo"/>
</template>
<script>
export defalut {
mounted{
this.$refs.demo.$on('xxx',this.test)
},
methods:{
test(事件参数1,...事件参数2-n){
}
}
}
</script>
若想让自定义事件只能触发一次,可以使用once修饰符,或$once方法。
触发事件:数据可以传递一个或多个,多个数据可以传递多个实参(第二个参数开始),或者写成一个对象
this.$emit('xxx',数据);
解绑事件
this.$off('xxx'); // 解绑指定事件
this.$off(['a','b']); // 解绑多个
this.$off(); // 解绑全部
注意:通过this.$refs.xxx.$on('aaa',回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题
销毁了当前的组件实例对象时,其身上的所有自定义事件全部销毁了;销毁了vue实例时,其所有的子组件全部销毁了,子组件的自定义世事件也被销毁了,但所有的原生事件不会销毁
三.全局事件总线
一种组件间通信的方式,适用于任意组件间通信。
安装全局事件总线:
new Vue({
......
beforeCreate() {
Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
},
......
})
由于vc.prototype.__ proto __ = vm.prototype,所以组件实例是可以访问到vue实例上的属性的,可以访问到$bus
使用事件总线
1. 接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身,事件的绑定一般在mounted钩子中进行,事件只需绑定一次即可,这和原生js中给DOM元素绑定事件的回调是一个道理。
methods(){
demo(data){......}
},
......
mounted() {
this.$bus.$on('xxxx',this.demo)
}
2. 提供数据
this.$bus.$emit('xxxx',数据)
最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
四.消息订阅和发布
一种组件间通信的方式,适用于任意组件间通信;
订阅方是数据的获取方,而发布方是数据的提供方;
使用步骤:
1. 安装:pubsub:npm i pubsub-js
2. 引入:import pubsub from 'pubsub-js'
3. 接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身。
methods(){
demo(data){......}
}
......
mounted() {
this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}
4.提供数据:
pubsub.publish('xxx',数据)
最好在beforeDestroy钩子中,用PubSub.unsubscribe(pid)去取消订阅。
五.vuex
1.概念
在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
2.使用场景
多个组件需要共享数据时
原理图:vuex的核心主要是由Actions,Mutations和state三部分组成,vuex做的是状态管理,实际上就是存放多个组件可以共享的数据,这些数据存放在state中,一般存放的可以是服务器返回的信息。
在下图中,Vue Components 就是vue组件,在组件中使用this.$store.dispatch('xxxAction') 调用某个action执行,一般在action中请求服务器接口(Backend API),获取请求得到的异步结果,但action无法直接操作state存放数据,所以此时它会调用指定的mutation,在mutation的内就可以操作state修改数据,最后在各个组件内使用this.$store.state.xxx 拿到state存放的状态;
当然,如果没有额外的业务逻辑需要处理,组件无需派发actions,可以在组件内直接使用this.$store.commit('xxxmutation'),通知mutations修改状态。
3.搭建vuex环境
0. 安装:npm i vuex
1. 创建文件:src/store/index.js
必须得在store实例创建前使用Vuex插件,所以Vue.use(Vuex)必须写在index.js中,而不是main.js中。
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)
//准备actions对象——响应组件中用户的动作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {}
// 准备getters 对state中的内容做二次处理,类似于计算属性
const getters = {}
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
state
})
2. 在main.js中创建vm时传入store配置项
//引入store
import store from './store'
......
//创建vm
new Vue({
el:'#app',
render: h => h(App),
store
})
这样就可以在vue实例和组件实例中看到$store属性了。
4.vuex的具体使用
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 = {
//执行加
IA(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
action中的方法第一个参数context还包括dispatch属性,即当业务逻辑较多可以链式调用多个action方法处理一个业务
const actions = {
//响应组件中加的动作
jia(context,value){
// console.log('actions中的jia被调用了',miniStore,value)
value = value + 1;
context.dispatch('jia1',value)
},
jia1(context,value) {
value = value * 2;
context.dispatch('jia2',value);
},
jia2(context,value) {
value = value * 3;
context.commit('JIA',value);
},
}
六.插槽
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于
父组件 ===> 子组件 。
分类:默认插槽、具名插槽、作用域插槽
使用方式:
1.默认插槽
父组件中:
<Category>
<div>html结构1</div>
</Category>
子组件中:
<template>
<div>
<!-- 定义插槽 -->
<slot>插槽默认内容...</slot>
</div>
</template>
2.具名插槽
一个组件多个插槽,给每个插槽命名加以区分,子组件name属性对应父组件的slot属性
父组件中:
<Category>
<template slot="center">
<div>html结构1</div>
</template>
<template v-slot:footer>
<div>html结构2</div>
</template>
</Category>
子组件中:
<template>
<div>
<!-- 定义插槽 -->
<slot name="center">插槽默认内容...</slot>
<slot name="footer">插槽默认内容...</slot>
</div>
</template>
3.作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定。(games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定)
具体编码:父组件通过scope属性值获取子组件的数据,作用域插槽和具名插槽可以结合使用。
父组件中:
<Category>
<template scope="scopeData">
<!-- 生成的是ul列表 -->
<ul>
<li v-for="g in scopeData.games" :key="g">{
{g}}</li>
</ul>
</template>
</Category>
<Category>
<template slot-scope="scopeData" slot="s1">
<!-- 生成的是h4标题 -->
<h4 v-for="g in scopeData.games" :key="g">{
{g}}</h4>
</template>
</Category>
子组件中:
<template>
<div>
<slot :games="games" name="s1"></slot>
</div>
</template>
<script>
export default {
name:'Category',
props:['title'],
//数据在子组件自身
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽']
}
},
}
</script>
七.v-model
v-model实现的是双向数据绑定,常用于表单元素;但也可以用于自定义组件标签上,实现父子组件的通信,父子组件间的数据同步。
首先这里用v-bind和input事件实现一个v-model
</template>
<input type="text" :value="text" @input="text=$event.target.value">
</template>
<script>
export default {
data(){
return{
text:0
}
},
}
其能实现v-model一样的效果,这里的value是input表单元素的属性,@input是表单的输入事件;如果一样的写法,写在组件标签上那么value就是组件的props参数,而@input则是自定义事件,若依旧i将@input作为原生的输入事件需要添加事件修饰符,即@input.native
接下去创建一个Inputs组件,给其传递一个参数value,并给其绑定一个自定义事件input
</template>
<Inputs :value="text" @input="text=$event"/>
<Inputs v-model="text"/>
</template>
<script>
export default {
data(){
return{
text:0
}
},
}
在组件内部接收参数value,触发自定义时间内input,将表单的输入内容$event.target.value作为自定义事件的参数传递。
<template>
<div>
<span>input:</span>
<input type="text" :value="value" @input="$emit('input',$event.target.value)">
</div>
</template>
<script>
export default {
props:['value']
}
</script>
其同样可以实现上面一样的双向数据绑定的效果,并且使用v-model替换组件的:value和@input也是等价的。
八.sync修饰符
可以实现父子组件数据的同步
<template>
<Money :money.sync="money"/>
<!--等价于 -->
<Money :money="money" @update:money="money=$event">
</template>
<script>
import Money from './components/Money.vue'
export default {
data(){
return {
money:10000
}
},
components:{
Money
}
}
</script>
:money.async代表父组件给子组件传递字符串props[money],给当前子组件绑定一个自定义事件(update:money),子组件如下:
<template>
<div>
<span>花费了100元</span>
<button @click="$emit('update:money',money-100)">花钱</button>
<span>还剩{
{money}}元</span>
</div>
</template>
<script>
export default {
props:['money']
}
</script>
九.$attrs和$listeners属性
$attrs是组件实例的一个属性,可以获取父组件传递过来的props数据;
$listeners是组件实例的属性,获取父组件给子组件传递的自定义事件;
如果子组件通过props接收的属性,在$attrs属性当中是获取不到的;
console.log(this.$attrs):
console.log(this.$listeners)
使用el-button封装一个组件,使其具有提示功能
父组件:
<template>
<ElButton type="success" icon="el-icon-delete" size="mini" title="按钮" @click="handler"/>
</template>
<script>
import ElButton from './components/elButton.vue'
export default {
components:{
ElButton
},
methods:{
handler(){
console.log('123')
},
}
}
</script>
子组件:可以采用type=”$attrs.type“ size="$attrs.size"......的写法,但像这样过于繁琐,如果传递的属性名和绑定标签的属性名是一致的,直接使用v-bind="$attrs"的形式,需要注意的是,不能写成:=''$attrs";
同理v-on不能用@进行替换
<template>
<a :title="title">
<el-button v-bind="$attrs" v-on="$listeners"></el-button>
</a>
</template>
<script>
export default {
props:['title'],
mounted(){
console.log(this.$attrs);
}
}
</script>
十.$children和$parent属性
$children是组件实例的属性,可以获取到当前组件的全部子组件,返回的是一个数组
$parent属性获取到某一组件的父组件,可以操作父组件的数据和方法
现在有如下示例:
点击父组件按钮,父组件的存款要加100,同时子组件的存款要减100,父组件通过$children获取子组件的实例,由于只存在一个子组件,所以$children的索引0就是子组件的实例,从而可以让子组件的data中的money减少100。
<template>
<div>
<h2>爸爸有存款:{
{money1}}</h2>
<button @click="borrowM(100)">找小明借钱100</button>
<Son/>
</div>
</template>
<script>
import Son from './components/Son.vue'
export default {
data(){
return{
money1:1000
}
},
methods:{
borrowM(money){
this.money1 += money;
this.$children[0].money -= money;
},
},
components:{
Son
}
}
如下,在子组件Son中,点击按钮,子组件中的存款减少50,同时父组件中的存款增加50,可以通过$parent获取父组件,从而操作父组件的data中的属性money1让其增加50. $parent返回的就是父组件实例对象。
<template>
<div>
<h3>儿子小明:现有存款:{
{money}}</h3>
<button @click="giveMoney(50)">给爸爸50元</button>
</div>
</template>
<script>
export default {
data(){
return {
money:1000
}
},
methods:{
giveMoney(money){
this.money -= money;
// 获取到父组件,让父组件的数据加50
this.$parent.money1 += money;
}
}
}
</script>